今日、libcurl [1] 内にもう 1 つの小さな変更を加えて、malloc の実行を少なくしました。今回は、汎用リンク リスト関数がより少ない malloc に変換されます (リンク リスト関数は本来そうあるべきです)。
malloc を研究する数週間前、私はメモリ割り当てについて調べ始めました。私たちは何年にもわたってメモリのデバッグとログ システムをカールで使用してきたので、これは簡単です。デバッグ バージョンのカールを使用し、ビルド ディレクトリで次のスクリプトを実行します:
リーリーcurl 7.53.1 の場合、これは約 115 のメモリ割り当てになります。これは多すぎるのでしょうか、それとも少なすぎるのでしょうか?
メモリのロギングは非常に基本的なものです。アイデアを提供するために、サンプル スニペットを次に示します:
リーリー ログを確認する次に、ログをさらに詳しく調べたところ、同じコード行で多数の小さなメモリ割り当てが行われていることがわかりました。明らかに、構造体を割り当て、その構造体をリンク リストまたはハッシュに追加し、そのコードで別の小さな構造体を追加するという非常に愚かなコード パターンがあり、これをループで行うことがよくあります。 (私がここで言っているのは、私たちです。誰も責めているわけではありません。もちろん、責任のほとんどは私にあります...)
これら 2 つの割り当て操作は常にペアで表示され、同時に解放されます。私はこれらの問題を解決することにしました。非常に小さい (32 バイト未満) 割り当てを行うことも無駄です。なぜなら、その小さなメモリ領域を追跡するために (malloc システム内で) 大量のデータが使用されるからです。瓦礫の山は言うまでもありません。
したがって、malloc を使用しないようにハッシュとリンク リストのコードを修正することは、最も単純な "curl http://localhost" 転送で 20% 以上の malloc を削除する迅速かつ簡単な方法です。
この時点で、すべてのメモリ割り当て操作をサイズ順に並べ替え、最小の割り当て操作をすべてチェックします。顕著な部分は curl_multi_wait() にあります。これは通常、メインの CURL 転送ループで繰り返し呼び出される関数です。ほとんどの典型的なケースでは、これをスタック [2] を使用するように変換します。関数呼び出しを何度も繰り返す場合は、malloc を避けるのが得策です。
再集計上記のスクリプトに示すように、同じ curl localhost コマンドは、curl 7.53.1 では 115 件の割り当て操作から、何も犠牲にすることなく 80 件の割り当て操作に減少しました。簡単に 26% 改善されます。悪くない、全く!
curl_multi_wait() を変更したので、もう少し高度な転送が実際にどのように改善されるのかも確認したいと思いました。 multi-double.c[3] サンプル コードを使用し、メモリ レコードを初期化する呼び出しを追加し、curl_multi_wait() を使用させ、2 つの URL を並行してダウンロードしました。 ## リーリー
2 番目のファイルは 512 メガバイトのゼロで、最初のファイルは 600 バイトのパブリック HTML ページです。これは count-malloc.c コード[4] です。
まず、7.53.1 を使用して上記の例をテストし、memanalyze スクリプトを使用してチェックしました。 リーリー わかりました。合計 160KB のメモリが使用され、33900 回以上割り当てられました。また、512 メガバイトを超えるデータをダウンロードするため、15 KB のデータごとに malloc が存在します。良いのか悪いのか?
git master に戻りますが、現在はバージョン 7.54.1-DEV です。次のバージョンをリリースするときのバージョン番号がどのようになるかはまったくわかりません。 7.54.1 または 7.55.0 である可能性がありますが、まだ確認されていません。話がそれましたが、同じ変更を加えた multi-double.c の例を再度実行し、メモリ ログに対して memanalyze を再度実行すると、次のようなレポートが得られます。 リーリー
信じられない気持ちで二度見してしまいました。どうしたの?再確認するには、もう一度実行した方がよいでしょう。何度実行しても結果は同じです。33961 vs 129
一般的な転送では、curl_multi_wait() が何度も呼び出され、転送中に少なくとも 1 つの通常のメモリ割り当て操作が実行されるため、その 1 つの小さな割り当て操作を削除すると、カウンターに非常に大きな影響を与えます。インパクト。通常の転送では、リンク リストの内外へのデータの移動やハッシュ操作も行われますが、現在ではほとんど malloc も含まれていません。簡単に言えば、残りの割り当て操作は転送ループでは実行されないため、ほとんど重要ではありません。
前のカールでは、現在の例の 263 倍の操作数が割り当てられました。言い換えれば、新しいものは古いものの割り当て操作の数の 0.37% です。
另外还有一点好处,新的内存分配量更少,总共减少了 7KB(4.3%)。
malloc 重要吗?在几个 G 内存的时代里,在传输中有几个 malloc 真的对于普通人有显著的区别吗?对 512MB 数据进行的 33832 个额外的 malloc 有什么影响?
为了衡量这些变化的影响,我决定比较 localhost 的 HTTP 传输,看看是否可以看到任何速度差异。localhost 对于这个测试是很好的,因为没有网络速度限制,更快的 curl 下载也越快。服务器端也会相同的快/慢,因为我将使用相同的测试集进行这两个测试。
我相同方式构建了 curl 7.53.1 和 curl 7.54.1-DEV,并运行这个命令:
curl http://localhost/80GB -o /dev/null
下载的 80GB 的数据会尽可能快地写到空设备中。
我获得的确切数字可能不是很有用,因为它将取决于机器中的 CPU、使用的 HTTP 服务器、构建 curl 时的优化级别等,但是相对数字仍然应该是高度相关的。新代码对决旧代码!
7.54.1-DEV 反复地表现出更快 30%!我的早期版本是 2200MB/秒增加到当前版本的超过 2900 MB/秒。
这里的要点当然不是说它很容易在我的机器上使用单一内核以超过 20GB/秒的速度来进行 HTTP 传输,因为实际上很少有用户可以通过 curl 做到这样快速的传输。关键在于 curl 现在每个字节的传输使用更少的 CPU,这将使更多的 CPU 转移到系统的其余部分来执行任何需要做的事情。或者如果设备是便携式设备,那么可以省电。
关于 malloc 的成本:512MB 测试中,我使用旧代码发生了 33832 次或更多的分配。旧代码以大约 2200MB/秒的速率进行 HTTP 传输。这等于每秒 145827 次 malloc - 现在它们被消除了!600 MB/秒的改进意味着每秒钟 curl 中每个减少的 malloc 操作能额外换来多传输 4300 字节。
去掉这些 malloc 难吗?一点也不难,非常简单。然而,有趣的是,在这个旧项目中,仍然有这样的改进空间。我有这个想法已经好几年了,我很高兴我终于花点时间来实现。感谢我们的测试套件,我可以有相当大的信心做这个“激烈的”内部变化,而不会引入太可怕的回归问题。由于我们的 API 很好地隐藏了内部,所以这种变化可以完全不改变任何旧的或新的应用程序……
(是的,我还没在版本中发布该变更,所以这还有风险,我有点后悔我的“这很容易”的声明……)
注意数字curl 的 git 仓库从 7.53.1 到今天已经有 213 个提交。即使我没有别的想法,可能还会有一次或多次的提交,而不仅仅是内存分配对性能的影响。
还有吗?还有其他类似的情况么?
也许。我们不会做很多性能测量或比较,所以谁知道呢,我们也许会做更多的愚蠢事情,我们可以收手并做得更好。有一个事情是我一直想做,但是从来没有做,就是添加所使用的内存/malloc 和 curl 执行速度的每日“监视” ,以便更好地跟踪我们在这些方面不知不觉的回归问题。
补遗,4/23(关于我在 hacker news、Reddit 和其它地方读到的关于这篇文章的评论)
有些人让我再次运行那个 80GB 的下载,给出时间。我运行了三次新代码和旧代码,其运行“中值”如下:
旧代码:
real 0m36.705s user 0m20.176s sys 0m16.072s
新代码:
real 0m29.032s user 0m12.196s sys 0m12.820s
承载这个 80GB 文件的服务器是标准的 Apache 2.4.25,文件存储在 SSD 上,我的机器的 CPU 是 i7 3770K 3.50GHz 。
有些人也提到 alloca() 作为该补丁之一也是个解决方案,但是 alloca() 移植性不够,只能作为一个孤立的解决方案,这意味着如果我们要使用它的话,需要写一堆丑陋的 #ifdef。
以上がCURL のメモリ割り当て操作を最適化するの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。