19

语境:

不久前,我偶然发现了 Alexandrescu 在 2001 年发表的这篇 DDJ 文章: http ://www.ddj.com/cpp/184403799

它是关于比较将缓冲区初始化为某个值的各种方法。就像“memset”对单字节值所做的一样。他比较了各种实现(memcpy、显式“for”循环、duff 的设备),并没有真正找到跨越所有数据集大小和所有编译器的最佳候选者。

引用:

这一切背后都有一个非常深刻和悲伤的认识。我们是在 2001 年,空间奥德赛之年。(...) 跳出框框看看我们——50 年后,我们仍然不擅长填充和复制记忆。

问题:

  1. 有没有人有关于这个问题的最新信息?最近的 GCC 和 Visual C++ 实现的性能是否明显优于 7 年前?
  2. 我正在编写生命周期为 5 年以上(可能 10 年以上)的代码,它将处理从几个字节到数百兆字节的数组大小。我不能假设我现在的选择在 5 年后仍然是最优的。我应该怎么办:
    • a) 使用系统的 memset(或等效项)并忘记最佳性能,或者假设运行时和编译器会为我处理这个问题。
    • b) 对各种数组大小和编译器一劳永逸地进行基准测试,并在运行时在多个例程之间切换。
    • c) 在程序初始化时运行基准测试,并在运行时根据准确的 (?) 数据进行切换。

编辑:我正在研究图像处理软件。我的数组项是 POD,每毫秒都很重要!

编辑2:感谢第一个答案,这里有一些附加信息:

  • 缓冲区初始化可能占某些算法总运行时间的 20%-40%。
  • 该平台在未来 5 年以上可能会发生变化,尽管它将保持在“可以从 DELL 购买的最快 CPU”类别中。编译器将是某种形式的 GCC 和 Visual C++。雷达上没有嵌入的东西或异国情调的架构
  • 我想听听那些在 MMX 和 SSE 出现时必须更新软件的人的意见,因为当“SSE2015”可用时我也必须这样做...... :)

4

12 回答 12

10

DDJ 文章承认 memset 是最好的答案,而且比他试图实现的要快得多:

C 的内存操作函数 memset、memcpy 和 memcmp 有一些神圣不可侵犯的地方。编译器供应商可能对它们进行了高度优化,以至于编译器可能会检测到对这些函数的调用并用内联汇编指令替换它们——MSVC 就是这种情况。

因此,如果 memset 适合您(即您正在使用单个字节进行初始化),那么请使用它。

虽然每毫秒都可能很重要,但您应该确定在设置内存方面浪费了多少执行时间。考虑到您还有有用的工作要做,它可能非常低(1% 或 2%??)。鉴于优化工作可能会在其他地方获得更好的回报率。

于 2008-10-05T12:58:45.010 回答
8

MASM 论坛有很多令人难以置信的汇编语言程序员/爱好者,他们已经彻底解决了这个问题(看看实验室)。结果很像 Christopher 的回答:SSE 对于大的、对齐的缓冲区来说是不可思议的,但是向下你最终会达到如此小的大小,以至于基本for循环也一样快。

于 2008-10-05T19:19:55.023 回答
5

Memset/memcpy 主要是在考虑基本指令集的情况下编写的,因此可以被专门的 SSE 例程超越,另一方面,它会强制执行某些对齐约束。

但要将其简化为列表:

  1. 对于 <= 几百千字节的数据集,memcpy/memset 的执行速度比您可以模拟的任何东西都快。
  2. 对于 > 兆字节的数据集,使用 memcpy/memset 的组合来获得对齐,然后使用您自己的 SSE 优化例程/回退到来自英特尔等的优化例程。
  3. 在启动时强制对齐并使用您自己的 SSE 例程。

此列表仅适用于您需要性能的事物。太小/或一旦初始化的数据集不值得麻烦。

是 AMD 的 memcpy 实现,我找不到描述代码背后概念的文章。

于 2008-10-05T13:53:19.767 回答
4

d) 接受尝试在初始化时玩“绝地思维技巧”会导致程序员损失更多的时间,而不是一些晦涩但快速的方法与一些明显而清晰的方法之间的累积毫秒差异。

于 2008-10-05T12:48:37.947 回答
4

这取决于你在做什么。如果您有一个非常具体的案例,您通常可以大大优于 memset 和 memcpy 的系统 libc(和/或编译器内联)。

例如,对于我正在处理的程序,我编写了一个 16 字节对齐的 memcpy 和 memset,专为小数据大小而设计。memcpy 仅适用于大于或等于 64 的 16 倍数(数据与 16 对齐),memset 仅适用于 128 倍数。这些限制让我获得了极大的速度,并且由于我控制了应用程序,我可以根据需要专门定制功能,还可以定制应用程序以对齐所有必要的数据。

memcpy 的执行速度大约是 Windows 原生 memcpy 的 8-9 倍,将 460 字节的副本缩短到仅 50 个时钟周期。memset 快了大约 2.5 倍,以极快的速度填充零堆栈数组。

如果您对这些功能感兴趣,可以在这里找到它们;对于 memcpy 和 memset,下拉到第 600 行左右。它们相当琐碎。请注意,它们是为应该在缓存中的小缓冲区而设计的;如果您想在绕过缓存的同时初始化内存中的大量数据,您的问题可能会更复杂。

于 2008-10-05T13:26:22.083 回答
2

您可以查看 liboil,它们(尝试)提供相同功能的不同实现并选择最快的初始化。Liboil 拥有相当自由的许可证,因此您也可以将其用于专有软件。

http://liboil.freedesktop.org/

于 2008-10-05T18:51:48.230 回答
1

As always with these types of questions, the problem is constrained by factors outside of your control, namely, the bandwidth of the memory. And if the host OS decides to start paging the memory then things get far worse. On Win32 platforms, the memory is paged and pages are only allocated on first use which will generate a big pause every page boundary whilst the OS finds a page to use (this may require another process' page to be paged to disk).

This, however, is the absolute fastest memset ever written:

void memset (void *memory, size_t size, byte value)
{
}

Not doing something is always the fastest way. Is there any way the algorithms can be written to avoid the initial memset? What are the algorithms you're using?

于 2008-10-06T08:19:37.850 回答
1

好吧,这一切都取决于您的问题域和您的规范,您是否遇到过性能问题,未能满足时间期限并将 memset 确定为万恶之源?如果是这样,那么您是唯一一种可以考虑进行 memset 调整的情况。

那么你还应该记住,memset 无论如何都会在它所运行的平台上的硬件上有所不同,在这五年中,软件会在同一个平台上运行吗?在同一架构上?如果您得出这个结论,您可以尝试“滚动您自己的”memset,通常使用缓冲区的对齐方式,确保根据您的架构上性能最高的内容一次将 32 位值归零。

我曾经遇到过相同的 memcmpt 对齐开销导致一些问题的情况,通常这不会导致奇迹,如果有的话,只有很小的改进。如果您在数量级上缺少您的要求,那么这不会让您更进一步。

于 2008-10-05T13:06:00.160 回答
1

如果内存不是问题,则预先创建所需大小的静态缓冲区,并初始化为您的值。据我所知,这两个编译器都在优化编译器,因此如果您使用简单的 for 循环,编译器应该生成最佳的汇编器命令来复制缓冲区。

如果内存有问题,请使用较小的缓冲区并将其在 sizeof(..) 偏移处复制到新缓冲区中。

高温高压

于 2008-10-05T13:44:53.217 回答
1

我总是会选择一种初始化方法,它是我正在使用的运行时或操作系统(memset)的一部分(更糟糕的情况是选择我正在使用的库的一部分)。

原因:如果您正在实现自己的初始化,您现在可能会得到一个稍微好一点的解决方案,但很可能在几年后运行时有所改善。而且您不想做与维护运行时的人一样的工作。

如果运行时的改进是微不足道的,那么所有这些都代表着。如果你在 memset 和你自己的初始化之间有一个数量级的差异,那么让你的代码运行是有意义的,但我真的怀疑这种情况。

于 2008-10-05T14:50:47.747 回答
1

如果您必须分配内存并对其进行初始化,我会:

  • 使用 calloc 代替 malloc
  • 将我的默认值尽可能多地更改为零(例如:让我的默认枚举值为零;或者如果布尔变量的默认值为“真”,则将其逆值存储在结构中)

这样做的原因是 calloc 为您零初始化内存。虽然这将涉及归零内存的开销,但大多数编译器可能会高度优化此例程——通过调用 memcpy 比 malloc/new 更优化。

于 2008-10-05T23:33:04.277 回答
0

这一年不再是 2001 年了。从那时起,出现了新版本的 Visual Studio。我花时间研究了其中的 memset。他们将 SSE 用于 memset(当然,如果可用)。如果您的旧代码是正确的,那么统计上 if 现在会更快。但是您可能会遇到不幸的情况。尽管我没有研究过代码,但我对 GCC 也有同样的期望。这是一个相当明显的改进,并且是一个开源编译器。有人会创建补丁。

于 2008-10-06T14:35:44.533 回答