【问题标题】:clearing a small integer array: memset vs. for loop清除一个小整数数组:memset vs. for 循环
【发布时间】:2010-11-11 04:05:33
【问题描述】:

有两种方法可以将整数/浮点数组归零:

memset(array, 0, sizeof(int)*arraysize);

或:

for (int i=0; i <arraysize; ++i)
    array[i]=0;

显然,memset 对于大型arraysize 更快。但是,memset 的开销实际上在什么时候大于 for 循环的开销?例如,对于大小为 5 的数组 - 哪个最好?第一个、第二个,甚至可能是展开的版本:

array[0] = 0;
array[1] = 0;
array[2] = 0;
array[3] = 0;
array[4] = 0;

【问题讨论】:

  • 好吧,我真的看不出有趣的部分。唯一真正的答案是通过在给定平台/编译器上对三个版本进行基准测试。此外,知道答案并没有真正的用处,不是吗?但是为了有效地编写基准代码;)
  • 是的,我打算自己做这个。但现在整个互联网都会知道! (至少对于带有 mingw 的 gcc 的 Windows...)
  • 我只是想知道你是怎么想到这个问题的?
  • 你没有提到第三版:int array[5] = {0};
  • 这仅用于在声明站点进行初始化。问题中的表格可随时用于清除现有数组。

标签: c performance benchmarking


【解决方案1】:

考虑到这个代码本身,一切都已经被告知了。但是如果你在它的程序中考虑它,我对此一无所知,可以做其他事情。例如,如果要每隔一段时间执行此代码以清除数组,您可以运行一个线程,该线程不断分配一个新的零元素数组,分配给全局变量,您的代码在需要清除数组时,简单地指向。

这是第三种选择。当然,如果您打算在至少有两个内核的处理器上运行代码,这是有道理的。此外,代码必须多次运行才能看到好处。对于一次性运行,您可以声明一个用零填充的数组,然后在需要时指向它。

希望这可以帮助某人

【讨论】:

  • 这仅适用于相当大的数组。在一个内核上归零并在另一个内核上使用所产生的缓存未命中开销,更不用说同步开销,对于小型阵列来说将是致命的。我猜想一个单独的归零线程只值得考虑大于 16kiB 的数组。 (现代 Intel CPU 中的 L1 缓存大小为 32kiB)
  • 按照您的建议释放旧数组并分配新的归零内存可能会更便宜,尤其是在大多数页面不会被写入的情况下。例如,在 Linux 上,来自 mmap 的新分配的页面都是写时复制映射到同一个归零内存的物理页面。 calloc(3) 利用了这一点,并且不会弄脏页面(与 std::vector 不同)。对每个页面的第一次写入将触发软页面错误。
  • 我以为每个内核都有自己的 L1 缓存,所以我无法知道会发生未命中的位置。不过,您可能比我更了解缓存。如果不经常清除阵列,同步开销应该不是问题。当清除线程持有锁时,另一个线程会做其他事情。真正的缺点是您需要非常清楚地知道在“主”线程中需要多久清除一次数组,我猜这并不总是可能的。如果不可能,我的想法就行不通,并且会大大减慢一切
  • 每个核心都有私有 L1,这正是问题所在:阵列在运行清理线程的核心上的 L1 缓存中会很热,而不是尝试使用它的核心。 memset 在使用它的同一线程中的一个小数组使该内存在 L1 中处于热状态,因此后续写入速度很快。如果它最后是由另一个线程写入的,它在共享的 L3 缓存中充其量是热的(自 Nehalem 以来的 Intel CPU 使用大型的包容性 L3 缓存,因此一致性流量不必通过主内存)。在最坏的情况下,它在另一个核心的 L1 (MOESI) 中仍处于 Modified 状态。
  • 对于大型数组,比如几个 MiB 或其他东西,维护一个归零数组池可能有点意义。但是,如果您比让操作系统为您做这件事并使用calloc 获得了很多好处,那么 IDK。对于较小的阵列,memset 非常便宜。如果您要在归零后立即触摸数组,您可能应该在同一个线程中执行这两项操作。
【解决方案2】:

没有衡量就无法回答这个问题。它将完全取决于编译器、cpu 和运行时库的实现。

memset() 可能有点“代码异味”,因为它可能容易出现缓冲区溢出、参数反转并且不幸地只能“按字节”清除。但是可以肯定的是,除了极端情况外,它在所有情况下都将是“最快的”。

我倾向于使用宏来包装它以避免一些问题:

#define CLEAR(s) memset(&(s), 0, sizeof(s))

这回避了大小计算并消除了交换长度和值参数的问题。

简而言之,在“幕后”使用 memset()。写下你想要的,让编译器担心优化。大多数人都非常擅长。

【讨论】:

  • ++ 天哪!你用宏!?最好躲起来!
  • 我认为您制作了宏参数 (x),但在实际正文中使用了 (s)……可能需要对其进行编辑。
  • @micmoo - 谢谢 - 已修复。 @mike re Macros:是的......但是它们在 C 中是不可避免的。C++ 对这个问题的回答会非常不同!
  • 你的宏会让你很难发现这样的代码问题:char* foo = malloc(80); CLEAR(foo);
  • @Roddy,在这个讨论中肯定有一些主观性的空间。以我的经验,像这样的宏调用增加了更多的混淆而不是它的价值。一位经验丰富的程序员在阅读您刚刚编写的 memset 调用时可能会立即采取双重措施。但是看到char* foo = malloc(80); CLEAR(foo); 中的错误需要知道CLEAR 的定义。 (可以说,有经验的程序员会立即知道 CLEAR 的合理定义对于动态分配的指针是不正确的;但这不是我想要依赖的那种经验。)
【解决方案3】:

memset() 很可能会被您的编译器内联(大多数编译器将其视为“内在”,这基本上意味着它是内联的,除非可能处于最低优化或除非明确禁用)。

例如,这里有一些release notes from GCC 4.3

方块移动的代码生成 (memcpy) 和块集 (memset) 被重写。 GCC 现在可以选择 最佳算法(循环,展开循环, 带有 rep 前缀的指令或 库调用)基于的大小 块被复制并且 CPU 被 为优化。一个新的选择 -minline-stringops-dynamically有 被添加。使用此选项字符串 未知大小的操作是 扩展使得小块 由行内代码复制,而对于 使用库调用的大块。 这导致代码比 -minline-all-stringops 当 库实现能够 使用缓存层次结构提示。这 启发式选择特定的 算法可以通过覆盖 -mstringop-strategy。新也 memset 的值不同于 0 是 内联。

编译器可能会使用您提供的替代示例执行类似的操作,但我敢打赌它不太可能。

它是grep-able,并且一目了然地启动的意图是什么(并不是说循环也特别难以理解)。

【讨论】:

  • 这是经常听到的“编译器在优化方面比你更好”的一个很好的例子。很少有应用程序程序员会在一次调用上花费如此多的注意力(如果他们这样做了,他们的实际应用程序可能会受到影响)。 :)
  • 放松,如果您认为“编译器比您更好”是您应该遵循的,请查看 - liranuna.com/sse-intrinsics-optimizations-in-popular-compilers
  • @LiraLuna - 这是一篇非常有趣的文章,但我认为你会同意 memset()/memcpy() 与 SSE 内在函数在编译器中的工作量不同代码生成。此外,您只想进行对真正对性能至关重要的代码(或者可能作为学术练习)所做的分析级别,因此值得您深入关注 - 不是针对每个缓冲区副本或清除.
  • @LiraNuna:6 年后更新:clang 目前拥有比 gcc 更强大的 SIMD 内在优化。特别是对于随机播放,clang 甚至不关心您首先使用的内在函数;它只是自己选择发出什么指令来实现相同的随机播放结果。这实际上可以be detrimental when you try to hand-tune a sequence and clang's current choices are imperfect,但在大多数情况下它很棒。 (而且将来,当 clang/LLVM 的优化器知道更多情况下的最佳 shuffle 时,这将不是问题。)
【解决方案4】:

正如 Michael 已经指出的那样,gcc 和我猜大多数其他编译器已经很好地优化了这一点。例如 gcc 转这个

char arr[5];
memset(arr, 0, sizeof arr);

进入

movl  $0x0, <arr+0x0>
movb  $0x0, <arr+0x4>

没有比这更好的了......

【讨论】:

  • 对于稍微大一点的数组,可以使用 64 位寄存器或 SIMD 一次将 8/16/32...字节归零
猜你喜欢
  • 1970-01-01
  • 2010-11-25
  • 2021-09-30
  • 2017-05-14
  • 1970-01-01
  • 2019-08-28
  • 1970-01-01
  • 1970-01-01
  • 2021-12-17
相关资源
最近更新 更多