Protobuf 作者不建议在 Deno 中使用 Protobuf
0. 布景我之前在”如何评价ry(Ryan Dahl)的新项目deno?”的回答中曾经写到:
我斗劲好奇的是 deno 使用了 Protobuf,而没有使用 Mojo。既然方针是要兼容浏览器,却不使用 Mojo...
...
如果想要兼容浏览器生态,选择 Mojo 是个捷径,而如果方针是高性能的处事器,那么应该选择非序列化的 zero-copy 库。无论从哪个角度看 protobuf 仿佛都不太适合 deno。
但是从 issue 中可以看出,Ryan Dahl 之前是没有风闻过 Mojo 的,但是他看完 Mojo 之后,依然感觉 Protobuf 是正确的的选择。Ryan Dahl 最初选择了 golang,后来又将 golang 从 deno 中彻底删除。前几天 Protobuf 的作者 Kenton Varda(kentonv) 开了一个 issue:Protobuf seems like a lot of overhead for this use case? #269,在文中 kentonv 指出:
I was surprised by the choice of Protobuf for intra-process communications within Deno. Protobuf's backwards compatibility guarantees and compact wire representation offer no benefit here, while the serialize/parse round-trip on every I/O seems like it would be pretty expensive.概略意思是:kentonv 对于 Deno 选择 Protobuf 感到很吃惊,因为 Protobuf 的兼容性优势并不是 Deno 需要的,相反,Protobuf 的序列化和反序列化非常消耗 I/O 性能。
1. Cap'n Proto 性能
kentonv 分开 Google 之后开发了 Cap'n Proto。
Cap'n Proto 对比 Protobuf 到底有多快呢?10 倍?100 倍?1000倍?官网给出了一张对比图:
Cap'n Proto 的编码解码速度是 Protobuf 的 ∞ 倍。2333
其实这张图是个标题党,图中对比了两者的编码解码,但是在 Cap'n Proto 中,是底子不需要编码和解码(encoding/decoding)的。Cap'n Proto 编码后的数据格式直接存放在内存,数据布局跟在内存里面的布局保持一致,所以可以直接将编码好的布局按照字节存放到硬盘,或者通过网络传输。
这是不是意味这 Cap'n Proto 编码是特定于平台的?
不!Cap'n Proto 采用的按字节编码方案是独立于任何平台的,但在如今主流的通用 CPU 上面会有更好的性能。数据的组织类似于编译器对 struct 的组织形式:固定宽度,固定偏移,以及内存对齐,对于可变的数组元素使用指针,而指针也是使用的偏移存放而不是绝对地址。整数使用的是小端序,因为大大都现代 CPU 都是小端序的,甚至大端序的 CPU 凡是有读取小端序数据的指令。
注:大端序(big-endian)和小端序(little-endian)统称为字节挨次。对于多字节数据,例如 32 位整数占据 4 字节,在分歧的措置器中存放方式也分歧,以内存中 0x0A0B0C0D 的存放方式为例:
在大端序中,如果数据以 8bit 为单元进行存储,则最高位字节 0x0A 存储在最低的内存地址处。
地址增长标的目的→
0x0A, 0x0B, 0x0C, 0x0D如果数据以 16bit 为单元进行存储,则最高的 16bit 单元 0x0A0B 存储在低位:
地址增长标的目的→
0x0A0B, 0x0C0D而小端序则与此相反。目前大大都主流 CPU 都是小端序的,这也是 Cap'n Proto 采用小端序的原因。
如果熟悉 C 或者 C++ 的布局体,可以看到 Cap'n Proto 的编码方式跟 struct 的内存布局很相似。即使在 V8 引擎内部,也是使用了类似的布局来进行属性的快速读取。对比使用 Hash Map 有很高的性能提升。
[*]扩展阅读:开启 V8 对象属性的“fast”模式
2. 序列化/反序列化
Protobuf 每次城市构建一个用于暗示 message 的对象,然后将对象序列化为 ArrayBuffer,在动静的接收方需要从缓冲区读取 message,然后解析为一个对应的对象,在之后的编程中使用该对象。而在 Cap'n Proto 中动静的布局直接存放在 ArrayBuffer 上,当我们调用 message.setFoo(123) 时,实际上就类似于 uint32Array = 123,在动静的接收方,我们可以直接从缓冲区读取这条动静。
Protobuf 可以使用变宽的编码,这样对于某些场景可以有更小的编码长度。而 Cap'n Proto 为了性能考虑会把整数编码为固定宽度,额外的字节使用 0 进行填充(这种存储方式很类似于 memcached)。一个是以空间换时间,一个是以时间换空间。在通过网络发送动静时,我们但愿动静体越小越好,但是如果在同一地址空间内通信时,则我们有无限带宽。Cap'n Proto 的文档中还指出,当带宽真的很重要时,无论您使用何种编码格式,都应该对动静体进行通用压缩,如 zlib 或 LZ4。
3. FlatBuffers
deno 的作者 ry 也在 issue 中参与了讨论,对大师的热情存眷 ry 感到十分打动,然后。。。。然后创建了一个 flatbuffers 分支 :P
FlatBuffers 同样是一个 created at Google 的库,具有更加完善的文档以及 Benchmarks。而 Cap'n Proto 除了阿谁“无限倍速”的不公平测试外,没有任何的基准测试数据。
而 kentonv 对基准测试的态度是:
关于基准测试 - 我花了很多时间对序列化系统进行基准测试,不幸的是,我的结论是基准测试成果几乎总是毫无意义。
...
一个真正有意义的基准测试,需要使用两种分歧的序列化来编写两个版本的实际应用法式,并对它们进行斗劲......但这是几乎没有人做过的大量工作。这确实是个大工程。对比而言,V8 和 Chrome 每次发布城市进行 Real-world JavaScript performance
4. 安全
在当前 deno 的 protobuf 使用上,每个动静城市创建一个副本。deno 使用 protobuf 只是为了在 V8 和其他特权代码之间通讯,即使真的明确需要一个动静副本,那么也可以直接使用 memcpy() 来达到更高的性能。
如果在同一个缓冲区(ArrayBuffer),当分歧的线程同时操作时,则需要一个副本来防止 TOCTOU 缝隙,或者谨慎的措置 JavaScript 代码,但这是不成控的,因为你不能防止第三方模块也做不异的假设(如果第三方扩展也使用不异的通讯机制的化)。
TOCTOU 的全程是“time of check to time of use”,TOCTOU 是竞争条件缺陷的一种。在多线程、多核系统中,这个缝隙很遍及。当我们访谒某个共享资源时,系统首先会检测当前用户或代码是否有权限,而查抄(check)和使用(use)是分手的,而且不是原子的。当系统查抄资源被授予用户权限后,攻击者可以临时阻塞调用户线程,然后在时间差内替换调资源,以达到越权访谒的目的。
举个简单的例子:
if (hasPermission(”file”)) {// (1)buffer = open(”file”);// (2) dosthwrite(”file”, buffer);// (3)
}而攻击者可以在 (1) 处构造如下代码:
// ...
// hasPermission 查抄通过
symlink(”/etc/passwd”, ”file”);
// 文件打开之前
// ...这样用户就越权拿到了 ”/etc/passwd” 的控制权。
上面只是一个简单的例子,TOCTOU 有很多分歧的形式。在类 Unix 系统上,/tmp 和 /var/tmp 目录经常会被错误地使用,从而导致竞争条件。
为了安全而暂时损掉性能是一种不得已的妥协,之前 V8 也遇到过,对于逃逸分析的缝隙直接导致了安全问题,Chrome 团队不得不不才一个发行版中去除了逃逸分析。
[*]扩展阅读:V8 团队的一个错误,使得整个互联网变慢
5. 综上
Deno 就像一个出生不久的孩子,Ryan Dahl 也在不竭的探索,不免会走一些弯路。而作为普通开发者的我们,可以存眷 deno 的源码以及 github 上的 commit。
对于一个非常成熟的项目,比如 Node.js,我们很难读懂他的全部源码,甚至我们都不知道从何读起。而 deno 则是一个机会,我们见证了 deno 的诞生,截至到我写这篇文章,deno 一共才有 249 次 commit。
页:
[1]