我的程序中是否可能存在'malloc'瓶颈?

3

我的程序每秒调用 malloc 函数 10,000 次。我完全不知道一个 malloc 调用需要多长时间。

  • 它需要像一个无竞争的互斥锁一样长吗?(10-100 纳秒)
  • 它需要像压缩 1KB 的数据一样长吗?(1-10 微秒)
  • 它需要像随机读取 SSD 一样长吗?(100-1000 微秒)
  • 它需要像传输到阿姆斯特丹的网络传输一样长吗?(10-100 毫秒)

为了不花两个小时去研究这个问题,只是去发现它在我程序的某些其他方面上被远远超过了,我希望大致了解该预计发生什么。可大可小的数量级都不要紧。

下图来自于 这里

enter image description here

1个回答

2
首先显而易见的是:对于特定用例,需要进行分析。然而,这个问题要求一个粗略的大致数量级估计。当我们不确定是否应该考虑一个问题时,我们会这样做。当我的数据被发送到阿姆斯特丹时,我需要担心它是否在缓存中吗?从问题的图片来看,答案是明确的“否”。是的,这可能是一个问题,但只有当你犯了很大的错误时才会出现。我们假设这种情况被排除了,而是以概率上的一般性来讨论问题。
或许有点讽刺的是,这个问题出现在我正在处理非常关注细节的程序中,其中几个百分点的性能差异就相当于数百万个CPU小时。分析表明malloc不是问题所在,但在完全排除之前,我想进行理性检查:malloc是否可能成为瓶颈?
正如在此问题的早期版本中反复建议的那样,不同的环境之间存在很大的差异。我尝试了各种机器(Intel:i7 8700K、i5 5670、一些笔记本电脑中的早期移动版i7;AMD:Ryzen 4300G、Ryzen 3900X)、各种操作系统(Windows 10、Debian、Ubuntu)和编译器(GCC、Clang-14、Cygwin-G++、MSVC;没有调试版本)。
我使用这个工具来了解特征,只使用1个线程:
#include <stddef.h>
#include <stdlib.h>
#include <time.h>
#include <stdio.h>

int main(int argc, char* argv[]) {
    const size_t allocs = 10;
    const size_t repeats = 10000;
    printf("chunk\tms\tM1/s\tGB/s\tcheck\n");
    
    for (size_t size = 16; size < 10 * 1000 * 1000; size *= 2) {
        float t0 = (float)clock() / CLOCKS_PER_SEC;
        size_t check = 0;
        
        for (size_t repeat = 0; repeat < repeats; ++repeat) {
            char* ps[allocs];
            for (size_t i = 0; i < allocs; i++) {
                ps[i] = malloc(size);
                if (!ps[i]) {
                    exit(1);
                }
                for (size_t touch = 0; touch < size; touch += 512) {
                    ps[i][touch] = 1;
                }
            }
            for (size_t i = 0; i < allocs; i++) {
                check += ps[i][0];
                free(ps[i]);
            }
        }
        
        float dt = (float)clock() / CLOCKS_PER_SEC - t0;
        printf ("%d\t%1.5f\t%7.3f\t%7.1f\t%d\n",
            size,
            dt / allocs / repeats * 1000,
            allocs / dt * repeats / 1000 / 1000,
            allocs / dt * repeats * size / 1024 / 1024 / 1024,
            check);
    }
}

方差很大,但是如预期的那样,这些值仍然属于同一范围。下表是代表性的,其他的偏差小于10倍。
chunk   ms      M1/s    GB/s    check
16      0.00003  38.052     0.6 100000
32      0.00003  37.736     1.1 100000
64      0.00003  37.651     2.2 100000
128     0.00004  24.931     3.0 100000
256     0.00004  26.991     6.4 100000
512     0.00004  26.427    12.6 100000
1024    0.00004  24.814    23.7 100000
2048    0.00007  15.256    29.1 100000
4096    0.00007  14.633    55.8 100000
8192    0.00008  12.940    98.7 100000
16384   0.00066   1.511    23.1 100000
32768   0.00271   0.369    11.3 100000
65536   0.00707   0.141     8.6 100000
131072  0.01594   0.063     7.7 100000
262144  0.04401   0.023     5.5 100000
524288  0.11226   0.009     4.3 100000
1048576 0.25546   0.004     3.8 100000
2097152 0.52395   0.002     3.7 100000
4194304 0.80179   0.001     4.9 100000
8388608 1.78242   0.001     4.4 100000

以下是cygwin-g++上3900X的测试结果。可以清晰地看到更大的CPU缓存,以及更高的内存吞吐量。

chunk   ms      M1/s    GB/s    check
16      0.00004  25.000     0.4 100000
32      0.00005  20.000     0.6 100000
64      0.00004  25.000     1.5 100000
128     0.00004  25.000     3.0 100000
256     0.00004  25.000     6.0 100000
512     0.00005  20.000     9.5 100000
1024    0.00004  25.000    23.8 100000
2048    0.00005  20.000    38.1 100000
4096    0.00005  20.000    76.3 100000
8192    0.00010  10.000    76.3 100000
16384   0.00015   6.667   101.7 100000
32768   0.00077   1.299    39.6 100000
65536   0.00039   2.564   156.5 100000
131072  0.00067   1.493   182.2 100000
262144  0.00093   1.075   262.5 100000
524288  0.02679   0.037    18.2 100000
1048576 0.14183   0.007     6.9 100000
2097152 0.26805   0.004     7.3 100000
4194304 0.51644   0.002     7.6 100000
8388608 1.01604   0.001     7.7 100000

那么问题来了?使用小的块大小,甚至在旧的普通硬件上每秒可以实现>= 1000万次调用。一旦大小超过CPU缓存,即1到100 MB左右,RAM访问将迅速占主导地位(我没有测试malloc而不实际使用块的情况)。根据您malloc的大小,其中一个将是(大致)限制。但是,对于每秒10k个分配,这可能是您可以暂时忽略的事情。


1
这肯定与各种系统上的内存分页大小有很大关系。一旦您开始要求大块内存,几乎每个malloc调用都需要分配、映射等新页面。小块可以在一页内满足。 - pm100
1
有一本由Web服务器团队编写的好书,其中他们追踪性能问题,并最终编写了自己的堆管理器。该管理器通过malloc在每个请求的开头获取足够大的内存块,然后从那时起进行管理。但我希望我能记住它的名字。 - pm100
@pm100 有趣的是,问题中的malloc调用是由我的自定义堆管理器发出的大块请求。这取代了数十亿个更小的malloc,并显著提高了运行时性能。 - mafu
你可以在实验中改变另一个参数,即malloc库本身;例如,可以看看 mimallocjemalloc - Warren Weckesser

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接