测试内存分配器:ptmalloc2 vs tcmalloc vs hoard vs jemalloc,同时尝试模拟真实世界的负载

本文主要是介绍测试内存分配器:ptmalloc2 vs tcmalloc vs hoard vs jemalloc,同时尝试模拟真实世界的负载,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

原文

当我们为参与者开发自己的分配器时,我们需要对其进行测试,更重要的是,需要将其与现有的分配器进行基准测试。显然,虽然有相当多的分配器测试程序,但这些测试中的大多数并不能代表真实世界的负载,因此,只能提供关于真实程序中分配器性能的非常粗略的想法。
例如,相当流行的t-test1.c(参见,例如,t-test1.c)经常被用作单元测试,因此它必须测试诸如calloc()realloc() 之类的东西;另一方面,这些功能在现代C++程序中很少使用,因此对它们进行基准测试实际上会对测试的真实世界条件(至少对于C++)跑偏。此外,均匀随机分布(分配的空间大小和创建/销毁项目的相对频率)虽然最容易模拟,但不能代表真实世界的情况(事实上,已知在大块内存上均匀随机分布是有效禁用缓存的最佳方式,但幸运的是,这在现实世界中很少发生。)最后但并非最不重要的一点是,只测试分配/取消分配,而不访问分配的内存,这是不现实的。

目标和要求

因此,我们决定开发自己的测试,尝试解决上面列出的所有问题。我们的目标是开发一个分配器的测试程序。一方面它将尽可能接近真实世界的C++程序,但是另一方面,它将允许测量存在的性能差异(没有退化成“所有分配器性能都是一样的,因为我们的测试在非分配代码中花费了太多的时间”)。当尝试进一步深入实际时,它产生了以下一组要求:

  • 我们希望我们的测试只使用new/delete(或malloc/free)。其他的东西在C++中很少使用,对它们做基准测试与“接近真实世界的C++程序”的目标不太一致。
    • 或者从另一个角度来看也是一样:如果你的程序真的需要很多realloc()调用,那么要为你的应用找到最佳的分配器,你需要一个不同的基准——很可能是一个不同的分配器。
  • 我们希望我们的测试使用一些真实场景的统计分布——既用于分配条目的大小的分布,也用于“分配条目的使用周期”的分布。
    • 我们确实意识到这必然是一个非常粗略的近似,但对于绝大多数程序来说,任何诚实的尝试都会比使用耗尽缓存(cache-killing)的一致分布更好。
  • 我们希望我们的测试能够访问所有分配的内存:我们应该至少写一次分配的内存,并且至少读回一次。
    • 我们承认,可以有不止一次的写/读操作,但我们认为至少一次是绝对最低限度的,目的是至少接近现实场景。

基本思想

从30000英尺的高度看,我们测试的一个非常基本的想法与t-test1.c非常相似:

  • 我们有一堆“槽(slot)”,每个“槽”代表一个潜在的分配项目。
  • 在每次迭代中,我们都会在槽上随机选择:
    • 如果它是空的(empty)–我们正在分配一个项目并将其放入槽中
    • 如果是释放的(free),我们将释放该项目,并将槽清空
  • 如果有多个线程,则每个线程都沿着上面的线独立工作。

差不多就是这样。

使分布更现实

在这个模型中,有两种不同的分布:(a)分配项目的大小分布,和(b)槽(slot)内随机选择的分布(这转化为分配项目的相对寿命分布)。
对于分配大小的分布,我们认为

p ~ 1/size

(其中“~”表示“成比例”且 size <= max_size)足以表示某种或多或少现实的场景。换句话说,分配 30字节的概率是分配 60字节概率的两倍,而分配60字节的概率又是分配 120字节概率的两倍,依此类推。在实践中,为了确保计算足够快,我们必须使用分段线性函数的近似值,但我们希望不要偏离1/size的目标太多。

对于选择用于操纵的槽(slot)分布,我们进行了大量实验,以确保一方面,访问频率较低的槽的概率快速降低(特别是,大大快于1/x,这太慢了,无法代表我们职业生涯中看到的真实世界分布),但另一方面,它们不会太快地达到虚拟零。最后,我们不得不求助于所谓的帕累托分布(在其经典版本中,“20%的人喝80%的啤酒”规则)。除了对于许多不同的非计算机相关的真实世界分布是一个非常好的近似值外,帕累托分布确实提供了我们认为相当接近真实世界的访问频率近似值;另外,我们观察到,在某些参数(槽数量和max_size)下,它与访问L1的次数 ~= 访问L2的次数 ~= 访问L3的次数 ~= 访问主RAM的次数没有太大的偏差,我们认为这是一个好迹象。

与分配的大小一样,我们确实需要使用分段线性函数,以便在测试时使事情变得合理快速,但我们确实希望这不是太糟糕。

访问分配的内存

如上所述,我们确实希望访问分配的内存;在我们的测试中,我们在分配之后编写整个块(使用memset())——并在释放之前读取它。这是我们在现实世界中所能想象的最低访问量(嘿,如果我们真的想分配,也许我们真的想在那里写点什么?)。

补偿机制

我们所做的所有工作(特别是关于分布区间)都在CPU方面取得了巨大的成功(尽管我们尽可能地避免了间接操作,但寄存器内(in-register)计算仍然非常重要。此外,我们确实想减去memset()和内存读取的理想情况成本,以便更清楚地看到不同分配器之间的差异。
为此,我们总是运行两个测试:(a)被测分配器,(b)虚拟“void”分配器(它完全不做任何事情),然后我们从(a)中花费的时间中减去(b)中花费的时间,以获得被测分配器的性能,而无需测试本身的成本。

测试

测试程序。我们运行我们的测试应用程序,按照上面讨论的思路设计,并在Github.alloc test上提供。
系统。我们在一个典型的2槽高的服务器上运行我们的所有测试,有两个E5645,每个E5645都有6个内核,具有超线程(Hyperthreading)(~=“2路SMT”),12M的三级缓存,能够以2.4GHz(涡轮模式下为2.67Ghz)的频率运行。这个主机有32G的RAM。OS:Debian 9“Stretch”(截至本文撰写之时,它是当前Debian“稳定的”版本)。非常简单:这是一个非常典型的(即使有点过时)现实世界的“主力”服务器。
正在测试的分配器。我们的第一组测试是在4个流行的Linux分配器上运行的:

  • 内置glibc分配器(一般认为它是经过大量修改的ptmalloc2)
  • [hoard]. 下载https://github.com/emeryberger/Hoard并编译
  • tcmalloc. apt-get install google-perftools
  • jemalloc. apt-get install libjemalloc-dev, apt-get install libjemalloc1

对于所有的分配器,我们没有使用LD_PRELOAD;相反,我们(对于除内置分配器外的所有分配器):

  • 编译我们的测试应用程序时使用了-fno-builtin-malloc-fno-builtin-calloc-fno-builtin-realloc-fno-builtin-free标志(如perftools.README中所建议的那样);在没有这些标志的情况下,我们至少在tcmalloc中遇到了问题。
  • 将我们的测试应用程序链接到-lhoard-ltcmalloc-ljemalloc

测试设置。为了运行我们的测试,我们必须选择某些参数,最重要的是——我们要分配的RAM数量。为了使我们的测试尽可能接近真实世界,我们决定使用大约1G的分配RAM进行测试;事实上,由于我们尽可能使用2的幂,我们最终得到了1.3G的分配RAM。需要注意的是,在处理多个线程时,我们决定在所有线程上拆分相同数量的RAM(这很重要,因为我们确实希望排除由于与CPU缓存相比使用的RAM大小不同而产生的差异)。
另一个参数(因测试而异)是线程数。由于我们的机器支持24个线程(2个CPU6个内核/CPU2个线程/内核=24个线程),我们使用1-23个线程运行测试(保留最后一个硬件线程以满足操作系统自身的需要,因此中断和后台活动不会对我们的测试产生太大影响)。

测量什么。对于第一组测试,我们只测试了两个参数:

  • 测试运行时花费的时间。然后,我们使用这些数据来推导“每个malloc()/free() 对的CPU时钟周期”指标。
  • 程序的内存使用情况(以RSS=“Resident Set Size”的最大值来衡量);在没有交换的情况下,它是程序从操作系统分配的RAM量的一个合理的良好指标。我们使用这些数据来计算“内存开销”(作为“从操作系统分配的RAM数量”与“应用程序级别通过malloc()请求的RAM数量”的比率)。

测试结果

现在,根据测试条件的描述(如果有什么遗漏,请给我们一个提示,我们很乐意进一步阐述),我们可以进入本文最有趣的部分——测试结果。
以下是我们得到的数据:
malloc test result
这或多或少与之前的测试和我们的预期一致,但是:

  • 由于我们试图模拟真实世界的行为,现代最先进的 Malloc 库之间的性能差异并不像有时所说的那样明显。特别是,tcmalloc 和 ptmalloc2 之间观察到的最大性能差异是,tcmalloc 的性能比 ptmalloc2 好1.7倍,尽管它只发生在一个线程上,并且在线程数量较多的情况下,ptmalloc2 的性能比 tcmalloc 好(高达1.2倍)
  • 值得注意的是,那些使用较少线程( tcmalloc 和 hoard)表现较好的 malloc,在大约12个线程时开始表现逐渐较差,到了大约18-20个线程时,表现比 ptmalloc2 和 jemalloc 差。
    • 这是否与超线程(SMT)有关(在本框中,12个线程是SMT发挥作用的一个点),还是与其他任何东西有关,都有待进一步研究。特别值得一提的是,如果我们在不同的进程中运行我们的测试线程,那么找出这种影响是否持续是很有趣的。

malloc overhead
我们测量的第二个参数是“内存开销”。这对于整体性能非常重要,因为我们产生的RAM开销越少,缓存的效率就越高。为了粗略估计这种影响,我们可以考虑分配程序的开销为2倍,实际上浪费了每一级缓存的一半,因此,相对于12M的三级缓存,使用这样一个开销为2倍的分配程序,我们将认为“好像”只有6M。
正如我们所看到的,从这个角度来看,jemalloc 是一个明显的赢家,ptmalloc2 是第二好的,而 tcmalloc 在任何线程数 > 1 的情况下都远远落后。

结论

现在,是时候从这些数据中得出我们主观的初步结论了:

  • 在不指定具体应用程序的情况下,我们测试的现代 malloc 库彼此之间性能差距不大,更重要的是,在某些条件下,它们中的每一个都可以超越另一个。换句话说:如果你确实想通过改变 malloc 获得性能,请确保用你自己的应用程序测试它们。
  • 如果我们不知道确切的应用程序(比如为OS发行版选择默认malloc),那么我们的建议如下:
    • 考虑到奇怪的内存开销,我们应该远离 hoard
    • tcmalloc 和 ptmalloc2 非常相似(在这一点上,tcmalloc 对于台式机和 ptmalloc2——对于服务器来说似乎有优势,但现在做出任何明确的结论还为时过早)
    • 但在我们的拙见中,到目前为止,总冠军是 jemalloc。它的低内存开销有望改善缓存的处理方式,对于“普通应用程序”,我们希望它的性能至少不会比其他应用程序差。你的情况可能会有所不同,上面的测试不包括电池消耗,请参阅上面的“确保使用您自己的应用程序测试电池”部分。

这篇关于测试内存分配器:ptmalloc2 vs tcmalloc vs hoard vs jemalloc,同时尝试模拟真实世界的负载的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/723669

相关文章

C++对象布局及多态实现探索之内存布局(整理的很多链接)

本文通过观察对象的内存布局,跟踪函数调用的汇编代码。分析了C++对象内存的布局情况,虚函数的执行方式,以及虚继承,等等 文章链接:http://dev.yesky.com/254/2191254.shtml      论C/C++函数间动态内存的传递 (2005-07-30)   当你涉及到C/C++的核心编程的时候,你会无止境地与内存管理打交道。 文章链接:http://dev.yesky

Javascript高级程序设计(第四版)--学习记录之变量、内存

原始值与引用值 原始值:简单的数据即基础数据类型,按值访问。 引用值:由多个值构成的对象即复杂数据类型,按引用访问。 动态属性 对于引用值而言,可以随时添加、修改和删除其属性和方法。 let person = new Object();person.name = 'Jason';person.age = 42;console.log(person.name,person.age);//'J

Windwos +vs 2022 编译openssl 1.0.2 库

一 前言 先说 结论,编译64位报错,查了一圈没找到解决方案,最后换了32位的。 使用qt访问web接口,因为是https,没有openssl库会报错 QNetworkReply* reply = qobject_cast<QNetworkReply*>(sender());if (reply){if (reply->error() == QNetworkReply::NoError

基于 Java 实现的智能客服聊天工具模拟场景

服务端代码 import java.io.BufferedReader;import java.io.IOException;import java.io.InputStreamReader;import java.io.PrintWriter;import java.net.ServerSocket;import java.net.Socket;public class Serv

将一维机械振动信号构造为训练集和测试集(Python)

从如下链接中下载轴承数据集。 https://www.sciencedirect.com/science/article/pii/S2352340918314124 import numpy as npimport scipy.io as sioimport matplotlib.pyplot as pltimport statistics as statsimport pandas

Android SurfaceFlinger——图形内存分配器(十一)

前面的文章中的图层合成器(HWC),这里我们接着看一下 SurfaceFlinger 中的另一个重要服务——图形内存分配器。 一、简介         android.hardware.graphics.allocator@2.0 是 Android 系统中硬件抽象层(HAL)的一个组件,专门用于图形内存的分配和管理。它是 SurfaceFlinger 在处理图形数据时所依赖的

逆向学习汇编篇:内存管理与寻址方式

本节课在线学习视频(网盘地址,保存后即可免费观看): ​​https://pan.quark.cn/s/3ceeb9ae6d98​​ 在汇编语言的世界中,内存管理和寻址方式是构建程序的基础。理解这些概念不仅对于编写高效的汇编代码至关重要,也是进行逆向工程分析的关键技能。本文将深入探讨内存管理的基本原则和多种寻址方式,并通过代码案例来展示它们的实际应用。 1. 内存管理 内存管理涉及如何分配

编译测试后出现“发现不明确的匹配”错误

原文链接:http://blog.163.com/zhaoyanping_1125/blog/static/201329153201204218533/ 错误提示: 【“/”应用程序中的服务器错误。  分析器错误 说明: 在分析向此请求提供服务所需资源时出错。请检查下列特定分析错误详细信息并适当地修改源文件。  分析器错误信息: 发现不明确的匹配。】   这个问题发生原因一般情况是

段,页,段页,三种内存(RAM)管理机制分析

段,页,段页         是为实现虚拟内存而产生的技术。直接使用物理内存弊端:地址空间不隔离,内存使用效率低。 段 段:就是按照二进制文件的格式,在内存给进程分段(包括堆栈、数据段、代码段)。通过段寄存器中的段表来进行虚拟地址和物理地址的转换。 段实现的虚拟地址 = 段号+offset 物理地址:被分为很多个有编号的段,每个进程的虚拟地址都有段号,这样可以实现虚实地址之间的转换。其实所谓的地

问题1,PE文件转到内存中出现解析PE不正确的问题

1,使用fopen(FileName, “r”) r的方式读取文件到内存,此时就可能存在问题了,r以只读方式,有时候不表示字符的有可能就不读了,那么内存中就不会是完整的原始文件。所以此时要采用rb,二进制读取的方式。 bool ReadFileToMem(char* FileName, char**buf) { FILE* f; f = fopen(FileName, “rb”); if