长生不老的RAM和末日的模板(2016)

2020-08-08 13:05:44

我将用两行代码试图说服您,Elixir比您使用过的任何编程语言都更有趣。

准备好了吗?不用担心,代码不涉及快速排序、元编程或任何类似的东西。

代码本身并没有什么特别之处。它打开一个文件并向其中写入一个短字符串,用HTML实体&;amp;替换(硬编码)字符串中的“&;amp;”。您可能足够敏锐,能够在几分钟或更短的时间内用您最喜欢的语言编写等价的代码。

但您的代码不会完全等同于药剂代码,至少从计算机的角度看不是这样。如果在非Elixir代码上运行strace或dtruss之类的跟踪程序,您可能会看到如下所示:

这只是我们期待的普通系统调用。但是如果你跟踪这个长生不老的程序,你会看到类似这样的东西:

什么给予?那个奇怪的十六进制数字是什么,更重要的是,为什么对于Fancy Pants药剂先生来说,好的旧笔迹(2)还不够好呢?

答案虽然从代码样例中并不是一目了然,但在解释Elixir独特的性能特征以及如果您尝试对Erlang或Elixir的HTML模板进行基准测试时可能会遇到的一些异常情况方面有很大帮助。继续阅读这个关于技术微妙和工程的故事,最终在内存中渲染末日的模板,一个来自海底的30G大小的怪物。在这篇文章结束时,我保证你不会以同样的方式看待任何网络服务器。

如果您将该代码粘贴到Elixir shell中,您将看到一些稍微出乎意料的东西:

我说的不是扁线,而是Erlang-Oops,我说的是Erlang吗?我的意思是elxir-创建一个包含四个叶元素的嵌套列表,它们加起来就是我们预期的字符串:";Hello";、";&;";、";amp;";和";再见";。乍一看,这似乎是一个毫无意义的复杂情况,但让我们看看计算机对这种情况的看法。

如果您查看writev的手册页,您将看到它是一个“聚集写入”,这意味着它在单个系统调用中从多个内存位置写入数据。我编写了一个小的DTrace脚本来解压前面看到的writev调用,并查看Elixir代码对这个系统调用实际做了什么。下面是该脚本的日志:

Writev:返回Writev数据1/4:(6字节):0x000000001a3c0d78 Hello writev:返回Writev数据2/4:(1字节):0x000000001a3c0d7e&;writev:返回Writev数据3/4:(4字节):0x000000001a3c0b49 amp;writev:返回Writev数据4/4:(8字节):0xx000000001a3c0b49 amp;writev:返回Writev数据4/4:(8字节):0xx000000001a3c0b49。

最初的十六进制数-在引言中的writev调用中-是向量的内存地址。该向量包含四个其他存储器地址的存储器地址,上面以大的十六进制数表示,紧挨着这些地址的字符串。

您可以从该日志中看到,Elixir正在单独编写嵌套列表的元素:";Hello";、";&;";、";amp;";和";再见";。但你有没有注意到记忆位置有什么特别之处?他们中有没有一个人看起来不像其他人呢?

我对十六进制不是很在行,所以让我们把所有字符都放在它们指定的内存地址上。为了清楚起见,我只是简单地重新排列和扩展上面日志中的数据-粗体-面向起始地址。

0x0b49';a';0x0b4a';m';0x0b4b';p';0x0b4c';;';...0x0d78';H';0x0d79';e';0x0d7a';l';0x0d7b';l';0x0d7b';0x0d7b';l';0x0d7b';l';0x0d7b';l';0x0d7b';G';0x0d81';o';0x0d82';o';0x0d83';o';0x0d84';d';0x0d85';b';0x0d86';y';0x0d87';e';

你看到了吗?字符串片段的嵌套列表-组成新字符串的列表-现在开始有意义了。它只是三个指向原始字符串的指针,加上一个指向替换字符串的乱序指针。换句话说,没有“新字符串”-只有旧字符串的一组修改。(您还可以看到正则表达式引擎执行的一个额外的微小优化-请注意,最终的字符串使用的是原始字符串中的和号,而不是替换字符串中的和号。)

数据结构称为I/O列表。它旨在利用writev,从而最大限度地减少写入磁盘或网络时的数据副本。大多数语言会删除原始字符串并复制整个内容,但是它们错过了一整类无复制(和延迟复制)的性能优化。

当然,指针不是万能的。有时数据复制比指针篡改更便宜。让我们使用DTrace来探索Erlang VM实现,并查看系统在试图平衡工程注意事项的过程中在哪里划清了不同的界限。