V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
爱意满满的作品展示区。
rapiz
V2EX  ›  分享创造

我用 Rust 重写(并重新设计)了 frp,性能和资源占用有很大改善

  rapiz ·
rapiz1 · 2022-01-04 18:53:16 +08:00 · 20934 次点击
这是一个创建于 815 天前的主题,其中的信息可能已经有所发展或是发生改变。

rathole-logo rathole - 安全、稳定、高性能的内网穿透工具,用 Rust 语言编写。


先博一下眼球,让各位有兴趣继续看下去:

  • TCP/UDP 最大吞吐量比 frp 高几倍
  • 内存占用从 60~70MiB 减少到 10MiB 以下( Rust vs Go ,这种结果并不意外)。
  • 二进制文件精简,且支持编译时开关特性裁剪,最小二进制在 x86_64 上大小只有 500KiB 左右
  • 连接延迟和 frp 基本相同,但能承载更高并发

如果你对性能和内存的测试数据感兴趣,可以直接拉到帖子下面的图表感受一下

你可以先看看 README ,其中介绍了特性、配置和用法


下面写写我为什么要做这个工具,和我开发的时候在想什么。

frp 是优秀的基于内网穿透反向代理工具,方便易用,集成了很多功能。

但我对一个内网穿透工具的期望不太一样,我相信一个工具应该专注做一件事,通过简单优雅的设计接口来提供扩展能力。

我仔细思考了如何设计这样一个工具,它应该:

  • 有安全和优雅的服务模型 服务端和客户端各自只负责自己的配置,并对外暴露配置 API (目前支持服务配置热重载,HTTP API 正在开发)。每个转发服务强制且单独鉴权。这样可以在易用性、安全性、灵活性间达到平衡:

    • 通过支持外置配置管理程序(比如使用 rsync 安全地同步配置),使单租户时像 frp 一样易用。
    • 服务配置分离。服务端 /客户端不需要有额外的信任关系
    • 每个服务使用单独的 Token 鉴权,同时也提供了所有服务的默认 Token 设置项。这样可以支持租户身份的任意划分,同时又支持单个用户时快速编写配置的需要。
  • 保持简单和专注 rathole 专注于内网穿透本身,而不是提供路由、复杂的鉴权和多租户管控、和各种运输层和应用层协议交互。这些需求应该通过和其他工具的组合实现,而不是集成到 rathole 中。这可以带来性能上的好处和更广泛的适用性,比如部署到嵌入式设备(路由器、工控机)中。

    • 比如基于域名的路由可以和 nginx 组合使用来优雅的实现。
    • 复杂的鉴权和多租户管控可以通过 API 来让外部程序实现一个 Dashboard 。实际上实现一个安全、高性能、且满足所有用户管理需求的单一内置 Dashboard 是不可能的事情。
    • 如果需要特殊的运输层和应用层协议承载流量,则应该通过对应的隧道工具实现。

相比于 frp ,除了数据上的优势,rathole 还有更多的可能性:

  • 低资源占用和特性可裁剪使其可能运行在中低端路由器、物联网设备上
  • 高性能使其更容易支撑高并发或者大流量的场景,更稳定

未来

rathole 已经持续开发了一段时间,必要的特性已经趋于完善和稳定。我觉得是时候发布出来,让大家体验使用和反馈。

目前已经决定的开发计划有 HTTP API (或其他形式的 API )。API 实现后,能够支持 Dashboard 和复杂动态管理需求的开发。

最后,如果你还没有查看 项目 README ,这里是一个方便的链接

性能测试

测试的具体数字随机器变化,测试方法见此,主要用 iperf3 测试 TCP/UDP 吞吐量, vegeta + nginx 测试 HTTP 吞吐量,同时检测内存占用

http_throughput tcp_bitrate udp_bitrate mem

117 条回复    2023-10-10 08:10:54 +08:00
1  2  
unique
    1
unique  
   2022-01-04 18:59:36 +08:00 via iPhone   ❤️ 2
star 先献上
ch2
    2
ch2  
   2022-01-04 19:03:10 +08:00
frp 的场景不太可能有很高的并发的,中转损失很大
chotow
    3
chotow  
   2022-01-04 19:03:41 +08:00 via iPhone   ❤️ 57
大佬甲:用 Rust 重写了 frp
大佬乙:用 UE5 制作了英雄联盟
我:什么时候过年放假
rapiz
    4
rapiz  
OP
   2022-01-04 19:07:19 +08:00
@ch2 你说的对,但工具总是可以打磨的,对于路由器这种设备来说意义尤其大。并且我相信大家总有种想省常驻服务的 CPU/内存占用的冲动吧。高性能另一面其实也意味着低负载下的低占用
adrianzhang
    5
adrianzhang  
   2022-01-04 19:14:17 +08:00 via iPhone
@rapiz 的确在路由上应用很广阔
laincat
    6
laincat  
   2022-01-04 19:20:45 +08:00 via iPhone   ❤️ 14
clash-rust 是不是指日可待了?
jackchenly
    7
jackchenly  
   2022-01-04 19:31:55 +08:00
开发了多长时间
ScepterZ
    8
ScepterZ  
   2022-01-04 19:41:38 +08:00
不了解 rust 现在支不支持路由器那些平台,建议在那些上边测测
FightPig
    9
FightPig  
   2022-01-04 19:42:38 +08:00
支持啊,rust 果然重写一切,哈哈
rapiz
    10
rapiz  
OP
   2022-01-04 19:43:30 +08:00
@jackchenly 快一个月吧,开发强度比较大。开始的时间要比 git init 要早
rapiz
    11
rapiz  
OP
   2022-01-04 19:44:51 +08:00
@FightPig 是的,常驻系统的服务,原来又是用 Go 写的,很难抵制住用 Rust 重写这类软件的诱惑!这是 Rust 擅长的领域。
tinkerer
    12
tinkerer  
   2022-01-04 19:45:23 +08:00
其他都好,就是 rathole 这名起得...
Reficul
    13
Reficul  
   2022-01-04 19:46:29 +08:00
Rust 在 IO 密集型应用上能比 Go 有这个差距还是比较意外的,话说有分析之前 FRP 的瓶颈在哪咩?
chenset
    14
chenset  
   2022-01-04 19:49:05 +08:00
@Reficul GC .. 哈哈哈哈
rapiz
    15
rapiz  
OP
   2022-01-04 19:56:05 +08:00 via Android
@Reficul 简单说一个吧,你可以看看 frp 转发 udp 部分的代码,服务端先把 udp 字节转成 base64 放到 string 里,string 放到一个结构体里,结构体序列化成 json ,经过 chan 在服务端内部传输,然后发给客户端,客户端再进行反序列化等等。抓 frp 转发 udp 的包看看,就能发现他传输的是 json 包 base64 。别的不说,就是带宽就肯定浪费了,base64 要比字节流长 1/4
mxT52CRuqR6o5
    16
mxT52CRuqR6o5  
   2022-01-04 19:56:06 +08:00
@Reficul
楼主说 [连接延迟和 frp 基本相同,但能承载更高并发]
就是说高并发时,CPU 成瓶颈了,变成 CPU 密集了
Reficul
    17
Reficul  
   2022-01-04 19:56:33 +08:00
@chenset 说 GC 我是不太信的...
rapiz
    18
rapiz  
OP
   2022-01-04 19:56:50 +08:00 via Android   ❤️ 1
@tinkerer Rust nAT HOLE 我本来的意思是,😂
Reficul
    19
Reficul  
   2022-01-04 19:59:41 +08:00
@rapiz 没研究过 FRP 的协议,如果是 JSON 的话,那慢很合理....

Go 的 JSON 库默认会把[]byte 转换到 Base64 后放进 JSON ,而且 JSON 本来就巨慢...
Rrrrrr
    20
Rrrrrr  
   2022-01-04 20:03:32 +08:00
居然输在名字,哈哈
manfred4527
    21
manfred4527  
   2022-01-04 20:14:11 +08:00
膜拜大佬
anxn
    22
anxn  
   2022-01-04 20:45:59 +08:00
名字可以再简化一点 这么长不太好记
shyangs
    23
shyangs  
   2022-01-04 20:47:52 +08:00
@anxn
Rat_Hole
鼠洞
shoaly
    24
shoaly  
   2022-01-04 20:58:14 +08:00
@rapiz 这么神奇么....竟然不是直接的 []byte.....
Alexonx
    25
Alexonx  
   2022-01-04 21:08:22 +08:00
有无可能在客户端增加配置后能自动同步到服务器端?假设 Token 为默认 Token 的情况下.(或者说这违背了`服务端 /客户端不需要有额外的信任关系`的原则?
rapiz
    26
rapiz  
OP
   2022-01-04 21:34:09 +08:00
@Alexonx 目前对于这种用例,我推荐用 rsync 脚本或者 lsyncd 同步配置文件。服务端和客户端的配置可以写到一个文件里。安全地内置这个 Feature 是比较难的。如果简单的让服务器信任发来的配置,选择的某种验证一旦被攻破,风险会比较大。举个例子,攻击者可以直接开服务器的 80 端口,然后通过 let's encrypt 的验证签一个对应域名的 HTTPS 证书出来。所以像 rsync 和 lsyncd 这种直接走 ssh ,安全性和 ssh 等同,是最优雅的方式。后面要不要做,怎么做我还要再想想。
zagfai
    27
zagfai  
   2022-01-04 22:46:39 +08:00
简单看了下,所以这速度也是受到了中间服务器的限制而不是直接的 P2P ?那这个不是跟直接 ssh 的反向隧道一个意义?
irytu
    28
irytu  
   2022-01-04 23:35:26 +08:00 via iPhone
在 tg 群里看过大佬推过 赞👍
kappa
    29
kappa  
   2022-01-05 00:02:16 +08:00
@Reficul
> Go 的 JSON 库默认会把[]byte 转换到 Base64 后放进 JSON

哪看的?
Goooler
    30
Goooler  
   2022-01-05 00:14:38 +08:00 via Android
你这个头像很 square 哦
ufan0
    31
ufan0  
   2022-01-05 00:22:38 +08:00   ❤️ 1
单从命名来说,要火的节奏。
littlewing
    32
littlewing  
   2022-01-05 00:52:42 +08:00
性能提升应该不是语言本身带来的吧,而是你优化的结果,或者说,go 版本经过优化,同样也可以达到这个吞吐量
区别就在于内存占用了
Losgif
    33
Losgif  
   2022-01-05 08:46:18 +08:00
@laincat 实际上可以有了,不过 clash 得自己封装一层。
miniliuke
    34
miniliuke  
   2022-01-05 08:56:45 +08:00
支持一下,但是你是怎么把二进制文件精简到 500K 的,有啥文档不......学习一下
superrichman
    35
superrichman  
   2022-01-05 09:15:44 +08:00
没有配置子域名访问的模式吗?
rapiz
    36
rapiz  
OP
   2022-01-05 09:17:28 +08:00 via Android
@miniliuke 500K 不算一个很惊人的水平,稍微注意一些就能做到。1. 这里有 Rust 程序二进制大小优化的一些技巧。https://github.com/johnthagen/min-sized-rust 2. 开发时谨慎地引入和选择依赖,观察二进制大小的变化。保持核心功能没有大的依赖,非核心功能(不是人人都会用到的)的依赖如果让程序膨胀很多,就提供编译时的控制开关让用户自己选择。Rust Cargo Feature 可以方便地做到这一点 3. 如何编译出这个项目的最小二进制可以看 docs/ 下的编译说明
rapiz
    37
rapiz  
OP
   2022-01-05 09:19:46 +08:00 via Android   ❤️ 2
@superrichman 这种场景,我建议使用 nginx 并将转发工具设置为 nginx 的 upstream ,这样无论是性能还是灵活性都比内置这个功能要好。内置 = 重新发明 nginx 。我不觉得我能超越 nginx 😂
CSGO
    38
CSGO  
   2022-01-05 09:21:55 +08:00 via Android
不是开发,一直在用 frp ,随看不懂,但是 666
codingbody
    39
codingbody  
   2022-01-05 10:20:34 +08:00
请教一下 OP ,这个 benchmark 的图是用什么工具画的呀。
rapiz
    40
rapiz  
OP
   2022-01-05 10:54:02 +08:00
@codingbody docs.google.com 内存图是 shell 脚本+gnuplot
275761919
    41
275761919  
   2022-01-05 11:30:23 +08:00
道理我都懂,为什么我的 frp 就只占用几 M 的内存
275761919
    42
275761919  
   2022-01-05 11:34:20 +08:00
frp 就只占用几 M 的内存![fpr 内存占用.jpg]( https://s2.loli.net/2022/01/05/N2rpFEufzgTHiw4.jpg)
missdeer
    43
missdeer  
   2022-01-05 11:39:55 +08:00
我主要用 frp 连远程桌面,所以对吞吐和并发的要求不高,对延迟比较敏感,因为 NAT 的关系不能用 UDP/KCP ,只能用 TCP ,继续观望一下
rapiz
    44
rapiz  
OP
   2022-01-05 12:20:43 +08:00
@275761919 benchmark 过程和脚本已经在 docs/benchmark.md 给出。这个结果是在有一定流量的时候测试的。不过我的 x86 frps 在即使没有任何连接的情况下也占了 20MiB 的 RSS 。你这个结果吓得我立刻去测试,发现还是有 20 MiB RSS 。你可以参照 benchmark 文档测试一下,或许和版本和统计口径有关。
bigbigpark
    45
bigbigpark  
   2022-01-05 13:49:31 +08:00
我想知道为啥用 Rust 写的 MeiliSearch 那么 吃内存。。
void1900
    46
void1900  
   2022-01-05 13:54:21 +08:00
我想知道的是稳定性如何,frp 长时间运行有时候会挂,也可能是 gfw 问题
rhacker1995
    47
rhacker1995  
   2022-01-05 13:55:20 +08:00
最近正好在学 rust ,希望楼主能出个 contribute guideline ,包括环境搭建,核心代码讲解,方便别人学习参与
zxxufo008
    48
zxxufo008  
   2022-01-05 14:06:03 +08:00
项目 logo rat 的不明显
HarveyLiu
    49
HarveyLiu  
   2022-01-05 14:34:48 +08:00
坐等官方 Docker
Ansen
    50
Ansen  
   2022-01-05 14:36:00 +08:00
看了一下语法,我还是老老实实的写 go[二哈]
0o0O0o0O0o
    51
0o0O0o0O0o  
   2022-01-05 14:46:51 +08:00 via iPhone
同在群里看到推过,鉴于 OP 说开发了快一个月,于是偷瞄了 OP 在群里近一个月的发言,只能说太强了
cat9life
    52
cat9life  
   2022-01-05 14:55:47 +08:00
一波三连 厉害了
carity
    53
carity  
   2022-01-05 15:06:32 +08:00
等一波 http api
learningman
    54
learningman  
   2022-01-05 15:08:23 +08:00
@ScepterZ #8 支持的,有交叉编译的工具链
lion9527
    55
lion9527  
   2022-01-05 15:09:35 +08:00
star 支持一个
jaylengao
    56
jaylengao  
   2022-01-05 15:38:36 +08:00
我一直想用 rust 整一个类似 frp 的项目,然而
feelinglucky
    57
feelinglucky  
   2022-01-05 15:49:36 +08:00   ❤️ 1
求官方的 docker 镜像,看了下配置和 frp 差不多
sgissb1
    58
sgissb1  
   2022-01-05 16:26:52 +08:00   ❤️ 1
其实用啥语言不重要,重要的是开发者敲键盘时,输入的代码逻辑和组织结构。

go 语言世界里,有不少项目是太过依赖语言特性了,这也导致了软件工程的表现各不一样。语言之争也是这么延续下来的,以前说 XX 语言好坏,那是硬件受限比较大,现在硬件都已经快不少了,软件基础设施也迭代不少了,互联网 ctrl+c ctrl+v 水平也在稳中趋升。还真单纯的纠结语言问题就没啥意思了,除非是某某语言创始人。
hxse
    59
hxse  
   2022-01-05 16:41:45 +08:00
可不可以添加一个"端到端加密"功能, 因为害怕中转服务器钓鱼, 所以不想把数据让中转服务器看到, 如果能支持这种加密就好了, 像 mega 网盘一样, 服务器方也不知道用户传了些什么
rapiz
    60
rapiz  
OP
   2022-01-05 16:45:36 +08:00
@hxse 对于端到端的场景,真正合适的工具是 VPN 和各种组网工具。取决于具体需求的不同,rathole 可能可以作为其中的一环,但不会负责全部。https://github.com/rapiz1/rathole/blob/main/docs/out-of-scope.md
Wincer
    61
Wincer  
   2022-01-05 17:38:38 +08:00
@bigbigpark #45 meilisearch 是挺占内存,可以试试 tantivy ,内存占用非常少
lwch
    62
lwch  
   2022-01-05 17:43:06 +08:00
https://github.com/jkstack/natpass
我也做了一个带内网穿透功能的产品,不过做到后面发现下沉到应用层( shell 和 vnc )会更炫酷一点。
hxse
    63
hxse  
   2022-01-05 17:48:27 +08:00
@rapiz #60 frp 和 vpn 配合有什么方案没有, 真的搞不懂, 网上好像也没什么案例
rapiz
    64
rapiz  
OP
   2022-01-05 17:55:12 +08:00
@lwch 我的想法恰恰相反,做一个简单快速的内网穿透工具,然后暴露一些 API ,让其他程序(比如一个 dashboard )方便地与之相互操作,而不是集成进去😂
rapiz
    65
rapiz  
OP
   2022-01-05 17:57:24 +08:00
@hxse 举一个最简单的例子,在想访问的主机上开一个 openvpn ,这个 openvpn 的端口在 NAT 后,公网无法访问,所以需要一个内网穿透工具把这个端口转发到有公网 IP 的主机上。具体的场景和需求因人而异。
nbndco
    66
nbndco  
   2022-01-05 18:04:27 +08:00   ❤️ 3
@sgissb1 理论上来说资源占用是水平问题,水平不行的人写 rust 也不会快。

但是如果水平到达一定程度的话,其实就涉及到一个语言特性的问题。

有的时候,项目一旦复杂,或者想要设计的很灵活,那很多问题就希望能够进行抽象,但是不同语言的抽象成本差距非常大。

像 rust 很多时候几乎是 zerocost 的抽象,使得我们几乎都不需要任何 Box 就可以实现非常灵活的功能。这在 C ,go 里面几乎无法想象,基本是原理上的不可能。在 C++里面,由于模版本身的限制,使得大规模的使用模版的开发维护难度非常高,在实际工程中几乎不可能,只能在极少数性能敏感的库中使用。

另外很多功能比如 Iterator ,使得一个自然设计的 API 在使用时就几乎不会产生不必要的性能损耗。这在其他语言中只是理论上可实现,可由于难度过大或者过于繁琐而没有任何人会去做。

这些都几乎没法通过开发者的水平来弥补。
binhb
    67
binhb  
   2022-01-05 18:12:16 +08:00 via iPhone
我又要开始 rust 入门了
des
    68
des  
   2022-01-05 18:49:10 +08:00 via iPhone
@rapiz #26 我也想要这个功能,考虑一下用 gpg 签名或者加密?
oneisall8955
    69
oneisall8955  
   2022-01-05 20:05:35 +08:00
赞👍,希望有异地组网类似 zerotier 路由表得功能
xiaolanger
    70
xiaolanger  
   2022-01-05 20:13:04 +08:00
@bigbigpark #45
@Wincer #61
一直好奇,melisearch 的使用体验怎么样?
LANB0
    71
LANB0  
   2022-01-05 20:22:19 +08:00
我又要开始 rust 从入门到放弃了
LANB0
    72
LANB0  
   2022-01-05 20:23:01 +08:00
既然叫 hole ,打洞技能有没有比 frp 升级一些
rapiz
    73
rapiz  
OP
   2022-01-05 20:49:24 +08:00
@feelinglucky 能讲讲 docker image 的场景是什么吗? rathole 是单文件二进制,直接使用应该会更方便些 🤔
jamry
    74
jamry  
   2022-01-05 23:02:05 +08:00 via iPhone   ❤️ 1
@rapiz 例如群晖、威联通等 nas 的内网穿透,可以基于 docker 跑 frp 实现
bufeii
    75
bufeii  
   2022-01-06 06:26:12 +08:00 via Android
@rapiz 先 Star 一个。我现在就是这种应用,入了一个 vps, SSH 转发的。Rust 是久闻大名。frp 这类还没用过,和 ssh 有啥区别?
Wincer
    76
Wincer  
   2022-01-06 10:19:56 +08:00   ❤️ 1
@xiaolanger meilisearch 在开箱即用的层面上做得还算可以,不过有些默认的配置我不是很喜欢并且无法更改,比如:1. 搜索是会全文返回,而不是返回片段; 2. typo tolerant 这个功能无法关闭,3. 内存占用比较大 等,所以我最后还是选择了基于 tantivy 定制一个搜索引擎,这样用的顺手一些
Reficul
    77
Reficul  
   2022-01-06 10:42:03 +08:00
@kappa

https://pkg.go.dev/encoding/json#Marshal

> Array and slice values encode as JSON arrays, except that []byte encodes as a base64-encoded string, and a nil slice encodes as the null JSON value.
rapiz
    78
rapiz  
OP
   2022-01-06 10:42:39 +08:00
xiaolanger
    79
xiaolanger  
   2022-01-06 10:45:59 +08:00
@Wincer #76 感谢回复,我也试一下 tantivy
kappa
    80
kappa  
   2022-01-06 11:08:06 +08:00
@Reficul 哦这是 marshal 一个[]byte ,实际场景上这么用很怪啊
ragnaroks
    81
ragnaroks  
   2022-01-06 12:43:16 +08:00
release 中的 rathole-x86_64-pc-windows-msvc.zip ,里面除了可执行文件外还有个 deps 文件夹,这里面的可执行文件和根目录下的有什么区别?
ragnaroks
    82
ragnaroks  
   2022-01-06 12:51:13 +08:00
在 windows 10 1809 (server2019 / 1809ltsc) 下似乎不能正常运行,键入 rathole.exe --help 或者其它开关都没有任何输出
ragnaroks
    83
ragnaroks  
   2022-01-06 13:40:23 +08:00
看下了错误码是 -1073741515 ,应该是 nanoserver 缺少依赖的 dll 导致的,servercore 是可以跑的
rapiz
    84
rapiz  
OP
   2022-01-06 16:16:20 +08:00
@ragnaroks 排查了一下,这来自使用的上游 Github Action 打包脚本的一个 Bug 。现在 release 已修复。 感谢反馈!
kknd22
    85
kknd22  
   2022-01-06 16:22:57 +08:00
frp 好像是中转吧,能先尽量尝试直连吗?
rapiz
    86
rapiz  
OP
   2022-01-06 22:34:00 +08:00   ❤️ 2
HarveyLiu
    87
HarveyLiu  
   2022-01-07 01:16:07 +08:00
@rapiz #86 点赞,效率,希望之后,维护,同步版本
zzl22100048
    88
zzl22100048  
   2022-01-07 13:25:57 +08:00   ❤️ 1
@xiaolanger meilisearch 不太适合大规模写入场景
xiaolanger
    89
xiaolanger  
   2022-01-07 14:02:24 +08:00
@zzl22100048 #88 写入不会太大,本来考虑的是 meilisearch 比 es 内存占用小,现在看来,好像不行?
rapiz
    90
rapiz  
OP
   2022-01-07 19:57:57 +08:00
@HarveyLiu 已经是 CI 触发的了,自动推送更新
sgissb1
    91
sgissb1  
   2022-01-10 23:09:50 +08:00
@nbndco 我不太认同你的观点。原因是逻辑和模块的设计实际上未必和性能挂钩。

真的能挂钩在一起的,我觉得更多是程序设计上的问题,我不认为有哪种语言可以做到绝对的灵活而且性能还能做到厉害到没什么语言可以打败的。

越简单就性能就是越好,越复杂就是性能很难做好。
首先 c 本来就不是面向对象,就没有太强的抽象能力,需要抽象能力的就要用 c 的近亲 c++(这里只谈论 c 和 c++)。那么很多时候都在说,c++的一些性能不如 c ,不是因为语言所致,而是模式所致( c++是部分面向对象,必然会有很多额外的开销,c 就不需要)。
go 接触过几天,rust 不了解,但据说语法各方面和 c++有一些相似。其实在我看来,你也可以去修改一个开源的 c++编译器,让编译出来的 release 代码做到最精简版本,你看性能好不好。另外 c++的开发维护难度高,还真不一定是模板问题,多数是 c++语言特性过于复杂,中二 c++程序员太多(过于自认为很了解 c++,搞出一堆问题来,不用模板,就一个继承和多态,在真实的项目中就能玩倒一片人)。

回到程序设计上来说,在程序设计的规模复杂度渐渐变大的时候,有时候会感到性能变差和维护变难。这个事情需要分两头看(排除写代码的人能力问题的话),一个是程序设计所致,另一个所谓的语言特性。

前者呢,可能是认为有一的变慢速度,比如一个事务,本身就要保证可靠性,那么一个巨大粒度的事务必然会比小粒度的慢。比如错误的使用多线程,导致竞争问题等等。
后者呢,比如一些代码需要编译成中间语言,运行的时候需要再做一次翻译,或者第一次运行需要翻译等等。

现在很多流行的语言高性能语言,说到底,就是把程序设计下沉到编译、解析层面。比如 go 的协程,就尽量的规避了中二程序员对线程在工程实践熟练度不高上引入的性能可能地下问题,然后由语言特性来包装起来,本质内部其实是固化了一定的程序设计模式在里面,简化和让程序员的程序设计模式更为确定和减少范围,以此来综合性的获得性能收益。所以 go 的早期版本里面为了解决调度、内存池、同步问题,自身的 runtime 里也有不少相关的 bug 在一点一点修改。

就像在 iot 或者低功耗设备里面,如果要用 python 、go 等等非 c/c++语言是完全可行的。但在一些特定功能或者模块上,用起来性能就有可能不如纯 c 的。为什么?一位内这些语言已经囊括了一定的程序设计模式,c 本身在这块会比他们更轻量级。

所以我还是觉得,本质还是程序设计的问题。过分强调某种语言如何如何我觉得没什么必要,关键在是好不好用,使用场景是否恰当,我记得这也是《设计模式》这本书里提到的一个重要观点。

我主要写 c++,但我也常常写 shell 和 python ,其实在很多地方,我就觉得 python 比 shell 、c++用起来更方便,shell 权限性大一些,c++太重。有些时候 python 性能不够的一些个人小服务,我会试着拿 go 来写,尽管 go 、的一些库的编译和依赖问题有时候让我比较抓狂,但奈何用起来方便,快捷。

真正商用场景,还是需要看哪一种语言用起来更合适,因为大家都有自己的特点。我以前也很喜欢推崇 c/c++,甚至觉得 vc 的方言版本就比 gnu 或者其他编译器的方言版本强的,但工程实践这么多年得到的经验,还是适用才最重要。
sgissb1
    92
sgissb1  
   2022-01-10 23:11:45 +08:00
@sgissb1@nbndco 原因是逻辑和模块的设计实际上未必和性能挂钩。

这句话我更正一下,没写清楚。

我是想说,语言是语言,程序设计是程序设计。性能的好坏更多是程序设计所致,语言不是决定因素。
rapiz
    93
rapiz  
OP
   2022-01-11 21:23:39 +08:00
@sgissb1 我觉得你可能需要学一下 Rust 再想想你这些观点。 @nbndco 关于零成本抽象的观点你好像理解的有点偏。
nbndco
    94
nbndco  
   2022-01-12 14:06:03 +08:00   ❤️ 2
@sgissb1

继承和多态都用不好的程序员完全不合格。我既然说了水平达到一定程度,这种入门的基础自然是已经完全理解并可以自然而然的应用的。我当然知道现在有些人自称程序员但是完全没入门,但是那个并不在考虑范围之内,rust 没有解决他们不会写正确的代码的问题,只是阻止了这些人写代码而已。

以下都假设是一个合格的程序员在写代码。

越简单就性能越好基本只对几百行的程序才可能为真(如果先不提 rust 的 zero cost abstraction 可能不会让代码变慢)。一旦代码复杂了之后,人脑能够理解并且不会发生错误的可能性太低了,必须要编译器来进行帮助。如果有些时候编译器帮助不了怎么办呢,只有两条路,要么就是写错误的代码,要么就是 over defensive (性能受损)。

我随便举两个例子,你既然对 C++比较熟悉,那么问:

1. 一个传入的指针,他的生命周期是什么?答案是,不知道,全靠文档和看源代码。

C++为了在语言层面上解决这个问题,只好加入 smart pointer ,但是并没有根本上解决这个问题,很多时候只是把所有的东西包在一个 rust 的 Arc<T>里了而已。

这带来两个问题:我们显然不能把一个传入的裸指针包进 shared_ptr 里,也就意味着所有的接口必须直接使用 shared_ptr ,即便某些情况下我们调用时事实上知道我们并不需要 shared_ptr ( enable_shared_from_this 我就不说了吧,基本给 shared_ptr 的正常使用判了死刑,说白了 smart pointer 除了 unique_ptr 以外其他的都完全没用);第二,如果接口没有用 shared_ptr 怎么办呢,所有权如此的不清晰使得我们只能把传入的数据拷贝一份,这个性能损耗就是非常惊人的了。

以上这些美好的设计还是建立在调用者和被调用者都正确地根据严谨清晰地文档实现了周期管理的前提下的,毕竟写错了也只有 runtime 的时候不时的报 segment fault 或者 leak 连错都没有。这点显然 Mozilla 的程序员水平完全不行,项目规模一大就不会写代码了,gecko 的内存泄漏问题一直都很严重,只好写个 rust 出来给自己擦屁股。

2. 一个类是否 thread-safe ?答案是,不知道,全靠文档和看源代码。

看文档,如果文档说了那就可以确定(吗?);如果文档没说,那么就只好去看源代码,如果没有源码,那就只能假设不 thread-safe 然后加锁了。为什么会这样呢?因为语言层面没有提供任何的机制来帮我们确定代码是否 thread-safe 。这个过程没必要的锁可能就不止一个了。没必要的锁可能发生在两层:1.类已经是 thread-safe 的了但我不知道; 2.虽然不是 thread-safe 但其实我们并没有任何 concurrent 的访问或者 mutable 的访问。

但是,老问题,以上这些美好的设计还是建立在调用者和被调用者都正确地根据严谨清晰地文档实现了 thread safety 的前提下。就算你今天是个优秀的程序员,通过仔细阅读代码确保了没有在一个 thread safe 的类访问再加一层锁,你有如何保证之后某个同事(甚至就是你自己)在修改这个类的时候把它改成不 thread-safe 的了呢?

我这几年写了一点 rust ,lifetime ,ownership 和 synchronization 基本都是我在 api 设计时候的本能反应了,依然还是会经常有各种编译器的错误,而且我看半天才能明白究竟哪里错了。

根本问题就出在,一旦代码复杂,人脑想要在 lifetime 和 synchronization 两个问题上想清楚不出错是不可能的,且不说你不可能通读所有代码并且不会想错任何东西,就说多人合作时大家各写各的改着改着你没动的代码就崩了。为了写出正确的代码,你只能 over defensive ,性能损耗随之而来。
nbndco
    95
nbndco  
   2022-01-12 14:10:20 +08:00
@sgissb1 以上所有的东西,我都没谈 Ergonomics 。

其实这个对性能影响也是根本性的。语言功能弱带来两个问题,一是代码可重用度低,从而优秀的算法不方便抽象成库广泛使用,二是正确的写法可能太麻烦,就懒得实现了。

但是理论上这不是性能损耗,因为你总是可以实现的么。只不过 rust 可能就是一两行,C 可能就要自己写几百行。
nbndco
    96
nbndco  
   2022-01-12 14:35:06 +08:00
@nbndco 忘了提抽象的问题了。

这些 over defensive 和 Ergonomics 本质上都是语言抽象能力弱,导致语义总是不清晰造成的。在 rust 里我可以写类似 `fn foo(arg: impl AsMut<dyn T + Send + Sync>)`来保证 thread safety 。这就使得我们可以在函数里避免所有无意义的锁(如果我们的类型不是 Send 和 Sync 的我们可以在调用的时候自己加上锁再传入就好了)。

在 C++里我就算放弃类型安全使用模版我都不知道该怎么实现这个函数。
nbndco
    97
nbndco  
   2022-01-12 14:42:32 +08:00
@nbndco 例子没仔细想,不一定 work ,可能要改成 `fn foo(arg: impl T + Send + Sync)` 更合理一点,但是意思是一样的。
sgissb1
    98
sgissb1  
   2022-01-12 17:21:00 +08:00
@nbndco 我觉得讨论的有偏了,我们讨论的焦点在于两个:抽象成本、性能;会引出这个的原因,在于你提到语言特性会影响(决定?)这两个因素,我提出的是,我认为主要因素在于人,语言特性是次要的。

首先我还是不认为某个语言特性就决定了他的性能,人的因素比较大,结论的原因在于:
理论和工程实践很多时候就不在一个视角上,我并不了解你工程实践中大多数是什么情况,但是我的情况是,不管是 x86 汇编、c 、c++,横向对比(特定语言中),人的因素远远大于语言特征。

另外你说的 c++标准库的这些东西,我看上去感觉你是想阐述程序设计中的问题,比如你提到的抽象成本问题。这方面,我是认同的,但是问题来了,这又会导致什么性能问题呢?在 tr1 特性或者说 c++11 出来之前,类似智能指针、作用于锁、安全对象和非安全对象等等就已经有不少实践了。我能看到的是,其他语言在这些方面上提供很多丰富 runtime api 来完善这些,但不代表他们的实现就是最优的,或者说,不是第一个版本的实现在当时主流硬件+软件环境就是性能最优,也是需要多次迭代来实现。

实践这么久,写代码,就是谁做得多问题,是语言 runtime 一侧多做一些抽象和设计,还是使用者这一侧多做一点。
nbndco
    99
nbndco  
   2022-01-12 18:42:52 +08:00
@sgissb1 我赞成主要是看人,这个是没有任何疑问的,所以我的前提一定是合格的程序员。不过我理解你说的问题,你说的对,确实大多数情况下,性能问题是不会由切换语言而解决的,不存在因为我是 rust 写的所以它就更快更省内存。我自己在写 rust 的过程中也知道有些东西只是理论上会快一点,但是这些东西在 rust 里面都是没有任何心智成本的(不论是维护上还是实现上)。也确实有一些场景由于 rust 本身的很多特性使得我可以大胆地进行很多优化。
sgissb1
    100
sgissb1  
   2022-01-13 10:56:51 +08:00
@nbndco 成功达成共识,不容易。感谢认同 ^_^
1  2  
关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   2830 人在线   最高记录 6543   ·     Select Language
创意工作者们的社区
World is powered by solitude
VERSION: 3.9.8.5 · 34ms · UTC 14:33 · PVG 22:33 · LAX 07:33 · JFK 10:33
Developed with CodeLauncher
♥ Do have faith in what you're doing.