Heute habe ich eine weitere kleine Änderung in libcurl[1] vorgenommen, damit weniger Mallocs ausgeführt werden. Dieses Mal werden generische verknüpfte Listenfunktionen in weniger Mallocs umgewandelt (so sollten verknüpfte Listenfunktionen eigentlich sein).
Research mallocVor ein paar Wochen habe ich begonnen, mich mit der Speicherzuweisung zu befassen. Das ist einfach, da wir in Curl seit Jahren Speicher-Debugging- und Protokollierungssysteme haben. Verwenden Sie die Debug-Version von Curl und führen Sie dieses Skript in meinem Build-Verzeichnis aus:
#!/bin/sh export CURL_MEMDEBUG=$HOME/tmp/curlmem.log ./src/curl http://localhost ./tests/memanalyze.pl -v $HOME/tmp/curlmem.log
Für Curl 7.53.1 sind dies etwa 115 Speicherzuweisungen. Ist das zu viel oder zu wenig?
Speicherprotokoll ist sehr einfach. Um Ihnen eine Vorstellung zu geben, hier ein Beispielausschnitt:
MEM getinfo.c:70 free((nil)) MEM getinfo.c:73 free((nil)) MEM url.c:294 free((nil)) MEM url.c:297 strdup(0x559e7150d616) (24) = 0x559e73760f98 MEM url.c:294 free((nil)) MEM url.c:297 strdup(0x559e7150d62e) (22) = 0x559e73760fc8 MEM multi.c:302 calloc(1,480) = 0x559e73760ff8 MEM hash.c:75 malloc(224) = 0x559e737611f8 MEM hash.c:75 malloc(29152) = 0x559e737a2bc8 MEM hash.c:75 malloc(3104) = 0x559e737a9dc8
Dann habe ich mich eingehender mit den Protokollen befasst und festgestellt, dass viele kleine Speicherzuweisungen in derselben Codezeile vorgenommen wurden. Wir haben offensichtlich einige ziemlich dumme Codemuster, bei denen wir eine Struktur zuweisen, diese Struktur dann zu einer verknüpften Liste oder einem Hash hinzufügen und dieser Code dann eine weitere kleine Struktur hinzufügt und so weiter, oft in einer Schleife. (Was ich hier sage, ist uns, niemandem die Schuld zu geben, natürlich liegt die meiste Verantwortung bei mir selbst...)
Diese beiden Zuordnungsvorgänge werden immer paarweise ausgeführt und gleichzeitig freigegeben. Ich habe beschlossen, diese Probleme zu lösen. Das Vornehmen sehr kleiner Zuweisungen (weniger als 32 Byte) ist ebenfalls verschwenderisch, da viele Daten (innerhalb des Malloc-Systems) verwendet werden, um den Überblick über diesen winzigen Speicherbereich zu behalten. Ganz zu schweigen von dem Trümmerhaufen.
Das Korrigieren dieses Hash- und verknüpften Listencodes, sodass Malloc nicht verwendet wird, ist eine schnelle und einfache Möglichkeit, über 20 % der Mallocs für die einfachste „Curl http://localhost“-Übertragung zu eliminieren.
An diesem Punkt sortiere ich alle Speicherzuweisungsvorgänge nach Größe und überprüfe alle kleinsten Zuweisungsvorgänge. Ein wichtiger Teil ist curl_multi_wait(), eine Funktion, die normalerweise wiederholt in der Haupt-Curl-Übertragungsschleife aufgerufen wird. Für die meisten typischen Fälle wandele ich dies in die Verwendung eines Stapels um [2]. Es empfiehlt sich, malloc bei vielen wiederholten Funktionsaufrufen zu vermeiden.
NachzählungWie im obigen Skript gezeigt, ist derselbe Befehl curl localhost von 115 Zuweisungsvorgängen mit Curl 7.53.1 auf 80 Zuweisungsvorgänge gesunken, ohne etwas zu opfern. Leicht eine Verbesserung um 26 %. Gar nicht so schlecht!
Da ich curl_multi_wait() modifiziert habe, wollte ich auch sehen, wie es tatsächlich einige etwas fortgeschrittenere Übertragungen verbessert. Ich habe den Beispielcode multi-double.c[3] verwendet, einen Aufruf zum Initialisieren des Speicherdatensatzes hinzugefügt, curl_multi_wait() verwendet und diese beiden URLs parallel heruntergeladen:
http://www.example.com/ http://localhost/512M
Die zweite Datei besteht aus 512 Megabyte Nullen, die erste Datei ist eine 600 Byte große öffentliche HTML-Seite. Dies ist der count-malloc.c-Code[4].
Zuerst habe ich 7.53.1 verwendet, um das obige Beispiel zu testen und es mit dem Skript memanalyze überprüft:
Mallocs: 33901 Reallocs: 5 Callocs: 24 Strdups: 31 Wcsdups: 0 Frees: 33956 Allocations: 33961 Maximum allocated: 160385
Okay, es wurden also insgesamt 160 KB Speicher und über 33900 Zuordnungsvorgänge verbraucht. Und es lädt über 512 Megabyte Daten herunter, sodass alle 15 KB Daten ein Malloc vorhanden ist. Ist es gut oder schlecht?
Zurück in Git Master ist es jetzt Version 7.54.1-DEV – da wir nicht ganz sicher sind, welche Versionsnummer es sein wird, wenn wir die nächste Version veröffentlichen. Es könnte 7.54.1 oder 7.55.0 sein, es ist noch nicht bestätigt. Ich schweife ab, ich habe das gleiche modifizierte multi-double.c-Beispiel noch einmal ausgeführt, memanalyze erneut für das Speicherprotokoll ausgeführt und hier kommt der Bericht:
Mallocs: 69 Reallocs: 5 Callocs: 24 Strdups: 31 Wcsdups: 0 Frees: 124 Allocations: 129 Maximum allocated: 153247
Ich habe es mir zweimal ungläubig angesehen. Was ist passiert? Um es noch einmal zu überprüfen, führe ich es besser noch einmal aus. Egal wie oft ich es ausführe, das Ergebnis ist immer noch dasselbe.
33961 vs. 129Bei einer typischen Übertragung wird curl_multi_wait() viele Male aufgerufen, und während der Übertragung findet normalerweise mindestens eine Speicherzuweisungsoperation statt, sodass das Entfernen dieser einzelnen winzigen Zuweisungsoperation einen sehr großen Einfluss auf den Zähler hat. Bei normalen Übertragungen werden auch einige Daten in und aus verknüpften Listen und Hashing-Vorgängen verschoben, aber sie sind jetzt auch größtenteils malloc-frei. Vereinfacht gesagt: Die restlichen Zuordnungsoperationen werden nicht in der Übertragungsschleife durchgeführt und sind daher von geringer Bedeutung.
Der vorherige Curl hat die 263-fache Anzahl an Operationen zugewiesen wie das aktuelle Beispiel. Mit anderen Worten: Die neue beträgt 0,37 % der Anzahl der Zuteilungsvorgänge der alten.
另外还有一点好处,新的内存分配量更少,总共减少了 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。
Das obige ist der detaillierte Inhalt vonOptimieren Sie den Speicherzuweisungsvorgang von Curl. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!