之前我们只是对代码进行了一些基础的单元测试,下面我们在多线程场景下对比 malloc 进行测试!
其中测试函数各个参数的含义如下:
ntimes:单轮次申请和释放内存的次数nworks:线程数rounds:轮次
在测试函数中,我们通过 clock 函数分别获取到每轮次申请和释放所花费的时间,然后将其对应累加到 malloc_costtime 和 free_costtime上。最后我们就得到了,nworks 个线程跑 rounds 轮,每轮申请和释放 ntimes 次,这个过程申请所消耗的时间、释放所消耗的时间、申请和释放总共消耗的时间。
注意,我们创建线程时让线程执行的是 lambda 表达式,而我们这里在使用 lambda 表达式时,以值传递的方式捕捉了变量 k,以引用传递的方式捕捉了其他父作用域中的变量,因此我们可以将各个线程消耗的时间累加到一起。
我们将所有线程申请内存消耗的时间都累加到 malloc_costtime 上, 将释放内存消耗的时间都累加到 free_costtime 上,此时 malloc_costtime 和 free_costtime 可能被多个线程同时进行累加操作的,所以存在线程安全的问题。鉴于此,我们在定义这两个变量时使用了 atomic 类模板,这时对它们的操作就是原子操作了。
// ntimes 一轮申请和释放内存的次数
// rounds 轮次
void BenchmarkMalloc(size_t ntimes, size_t nworks, size_t rounds)
{
std::vector<std::thread> vthread(nworks);
std::atomic<size_t> malloc_costtime = 0;
std::atomic<size_t> free_costtime = 0;
for (size_t k = 0; k < nworks; ++k)
{
vthread[k] = std::thread([&, k]() {
std::vector<void*> v;
v.reserve(ntimes);
for (size_t j = 0; j < rounds; ++j)
{
size_t begin1 = clock();
for (size_t i = 0; i < ntimes; i++)
{
v.push_back(malloc(16));
//v.push_back(malloc((16 + i) % 8192 + 1));
}
size_t end1 = clock();
size_t begin2 = clock();
for (size_t i = 0; i < ntimes; i++)
{
free(v[i]);
}
size_t end2 = clock();
v.clear();
malloc_costtime += (end1 - begin1);
free_costtime += (end2 - begin2);
}
});
}
for (auto& t : vthread)
t.join();
printf("%u个线程并发执行%u轮次,每轮次malloc %u次: 花费:%u ms\n",
nworks, rounds, ntimes, malloc_costtime.load());
printf("%u个线程并发执行%u轮次,每轮次free %u次: 花费:%u ms\n",
nworks, rounds, ntimes, free_costtime.load());
printf("%u个线程并发malloc&free %u次,总计花费:%u ms\n",
nworks, nworks * rounds * ntimes, malloc_costtime.load() + free_costtime.load());
}
// 单轮次申请释放次数 线程数 轮次
void BenchmarkConcurrentMalloc(size_t ntimes, size_t nworks, size_t rounds)
{
std::vector<std::thread> vthread(nworks);
std::atomic<size_t> malloc_costtime = 0;
std::atomic<size_t> free_costtime = 0;
for (size_t k = 0; k < nworks; ++k)
{
vthread[k] = std::thread([&]() {
std::vector<void*> v;
v.reserve(ntimes);
for (size_t j = 0; j < rounds; ++j)
{
size_t begin1 = clock();
for (size_t i = 0; i < ntimes; i++)
{
v.push_back(ConcurrentAlloc(16));
//v.push_back(ConcurrentAlloc((16 + i) % 8192 + 1));
}
size_t end1 = clock();
size_t begin2 = clock();
for (size_t i = 0; i < ntimes; i++)
{
ConcurrentFree(v[i]);
}
size_t end2 = clock();
v.clear();
malloc_costtime += (end1 - begin1);
free_costtime += (end2 - begin2);
}
});
}
for (auto& t : vthread)
t.join();
printf("%u个线程并发执行%u轮次,每轮次concurrent alloc %u次: 花费:%u ms\n",
nworks, rounds, ntimes, malloc_costtime.load());
printf("%u个线程并发执行%u轮次,每轮次concurrent dealloc %u次: 花费:%u ms\n",
nworks, rounds, ntimes, free_costtime.load());
printf("%u个线程并发concurrent alloc&dealloc %u次,总计花费:%u ms\n",
nworks, nworks * rounds * ntimes, malloc_costtime.load() + free_costtime.load());
}
int main()
{
size_t n = 1000;
cout << "==========================================================" << endl;
BenchmarkConcurrentMalloc(n, 4, 10);
cout << endl << endl;
BenchmarkMalloc(n, 4, 10);
cout << "==========================================================" << endl;
return 0;
}当固定大小内存的申请和释放时:
debug 环境下:

release 环境下:

由于此时我们申请释放的都是固定大小的对象,每个线程申请释放时访问的都是各自 thread cache 的同一个桶,当 thread cache 的这个桶中没有对象或对象太多要归还时,也都会访问 central cache 的同一个桶。此时 central cache 中的桶锁就不起作用了,因为我们让 central cache 使用桶锁的目的就是为了,让多个 thread cache 可以同时访问 central cache 的不同桶,而此时每个 thread cache 访问的却都是 central cache 中的同一个桶。
当不同大小内存的申请和释放时:
debug 环境下:

release 环境下:

运行后可以看到,由于申请和释放内存的大小是不同的,此时 central cache 当中的桶锁就起作用了,ConcurrentAlloc 的效率也有了较大增长,但相比 malloc 来说还是差一点点。