我以前在"如何评价ry(Ryan Dahl)的新项目deno?"的回答中曾经写到:javascript
我比较好奇的是 deno 使用了 Protobuf,而没有使用 Mojo。既然目标是要兼容浏览器,却不使用 Mojo......html
可是从 issue 中能够看出,Ryan Dahl 以前是没有据说过 Mojo 的,可是他看完 Mojo 以后,依然以为 Protobuf 是正确的的选择。java
Ryan Dahl 最初选择了 golang,后来又将 golang 从 deno 中完全删除。前几天 Protobuf 的做者 Kenton Varda(kentonv) 开了一个 issue:Protobuf seems like a lot of overhead for this use case? #269,在文中 kentonv 指出:git
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 性能。github
kentonv 离开 Google 以后开发了 Cap'n Proto。golang
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 有很高的性能提高。
Protobuf 每次都会构建一个用于表示 message 的对象,而后将对象序列化为 ArrayBuffer,在消息的接收方须要从缓冲区读取 message,而后解析为一个对应的对象,在以后的编程中使用该对象。而在 Cap'n Proto 中消息的结构直接存放在 ArrayBuffer 上,当咱们调用 message.setFoo(123)
时,实际上就相似于 uint32Array[offset] = 123
,在消息的接收方,咱们能够直接从缓冲区读取这条消息。
Protobuf 可使用变宽的编码,这样对于某些场景能够有更小的编码长度。而 Cap'n Proto 为了性能考虑会把整数编码为固定宽度,额外的字节使用 0 进行填充(这种存储方式很相似于 memcached)。一个是以空间换时间,一个是以时间换空间。在经过网络发送消息时,咱们但愿消息体越小越好,可是若是在同一地址空间内通讯时,则咱们有无限带宽。Cap'n Proto 的文档中还指出,当带宽真的很重要时,不管您使用何种编码格式,都应该对消息体进行通用压缩,如 zlib 或 LZ4。
deno 的做者 ry 也在 issue 中参与了讨论,对你们的热情关注 ry 感到十分感动,而后。。。。而后建立了一个 flatbuffers 分支 :P
FlatBuffers 一样是一个 created at Google 的库,具备更加完善的文档以及 Benchmarks。而 Cap'n Proto 除了那个“无限倍速”的不公平测试外,没有任何的基准测试数据。
而 kentonv 对基准测试的态度是:
关于基准测试 - 我花了不少时间对序列化系统进行基准测试,不幸的是,个人结论是基准测试结果几乎老是毫无心义。...
一个真正有意义的基准测试,须要使用两种不一样的序列化来编写两个版本的实际应用程序,并对它们进行比较......但这是几乎没有人作过的大量工做。
这确实是个大工程。相比而言,V8 和 Chrome 每次发布都会进行 Real-world JavaScript performance
在当前 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) dosth write("file", buffer); // (3) }
而攻击者能够在 (1)
处构造以下代码:
// ... // hasPermission 检查经过 symlink("/etc/passwd", "file"); // 文件打开以前 // ...
这样用户就越权拿到了 "/etc/passwd"
的控制权。
上面只是一个简单的例子,TOCTOU 有不少不一样的形式。在类 Unix 系统上,/tmp
和 /var/tmp
目录常常会被错误地使用,从而致使竞争条件。
为了安全而暂时损失性能是一种不得已的妥协,以前 V8 也遇到过,对于逃逸分析的漏洞直接致使了安全问题,Chrome 团队不得不在下一个发行版中去除了逃逸分析。
Deno 就像一个出生不久的孩子,Ryan Dahl 也在不停的探索,不免会走一些弯路。而做为普通开发者的咱们,能够关注 deno 的源码以及 github 上的 commit。
对于一个很是成熟的项目,好比 Node.js,咱们很难读懂他的所有源码,甚至咱们都不知道从何读起。而 deno 则是一个机会,咱们见证了 deno 的诞生,截至到我写这篇文章,deno 一共才有 249 次 commit。