在32位嵌入式平台上,memset速度较慢

4
我正在开发一个嵌入式设备(STM32,ARM-Cortex M4),期望memset和类似函数进行速度优化。然而,我注意到它的行为比预期要慢得多。我正在使用GNU ARM嵌入式编译器/链接器(arm-none-eabi-gcc等)以-O3优化标志。
我查看了反汇编代码,发现memset函数是每次写一个字节并在每次迭代时重新检查边界。
0x802e2c4 <memset>: add r2, r0
0x802e2c6 <memset+2>:   mov r3, r0
0x802e2c8 <memset+4>:   cmp r3, r2
0x802e2ca <memset+6>:   bne.n   0x802e2ce <memset+10>
0x802e2cc <memset+8>:   bx  lr
0x802e2ce <memset+10>:  strb.w  r1, [r3], #1
0x802e2d2 <memset+14>:  b.n 0x802e2c8

自然地,这段代码可以通过使用32位写入和/或循环展开来加速,但代价是增加代码大小。实现者可能选择不为了速度进行优化,以保持代码大小。

memset头文件和库被包括在:

C:\Program Files (x86)\GNU Tools Arm Embedded\7 2018-q2-update\arm-none-eabi\include\string.h
C:\Program Files (x86)\GNU Tools Arm Embedded\7 2018-q2-update\arm-none-eabi\include\c++\7.3.1\cmath

这个问题类似于现有的问题,但不同之处在于它针对嵌入式平台。

在GNU ARM嵌入式软件包中是否有可用的优化memset函数?如果有,如何访问它?


2
如果你正在使用C++进行开发(如标签所示),最好使用std::fill。这有很大可能会被编译器优化。 - SergeyA
1
“newlib” C库的源代码已经针对速度进行了优化,并进行了32位写操作。仅当内存地址正确对齐时才会触发此优化,请确保情况如此。来自基于newlib的GCC-ARM-Embedded发布的libc.a变体都使用了该优化。您使用哪个发布版本?你有什么源代码,如何编译/链接呢? - Erlkoenig
1
@SergeyA std::fill的性能与memset相同。为什么有人会期望它比memset更好地优化呢? - devtk
@devtk 因为memset(除非是内置函数)是由库提供的函数,它是什么就是什么,可能是通用的。当您使用std::fill时,编译器有优化现场的奢侈品。然而,我必须承认,在尝试时,我在godbolt上没有看到任何优化。 - SergeyA
我确认arm-none-eabi_gcc的newlib-nano标准库C在memcpy和memset时是逐字节复制或写入的。 - Guillaume Petitjean
显示剩余7条评论
2个回答

4

不确定 GNU Tools ARM Embedded 是否有优化的 memset 函数, 或者如何通过链接选项访问它, 但可以手动用汇编优化。定义后,链接器使用此版本而不会抱怨重新定义函数,这对我来说似乎很奇怪。总体速度提高约9倍(即此版本所需时间约为原始逐字节方法的11%)。

// optimized version of memset
// we split up the region into several segments
//
// base_ptr
// * store single bytes
// mid1
// * store words, 4 at a time
// mid2
// * store words, 1 at a time
// mid3
// * store single bytes
// end
//
// For large buffers, most of the time is spent between mid1 and mid2 which is
// highly optimized.
void * memset(void * base_ptr, int x, size_t length) {
  const uint32_t int_size = sizeof(uint32_t);
  static_assert(sizeof(uint32_t) == 4, "only supports 32 bit size");
  // find first word-aligned address
  uint32_t ptr = (uint32_t) base_ptr;
  // get end of memory to set
  uint32_t end = ptr + length;
  // get location of first word-aligned address at/after the start, but not
  // after the end
  uint32_t mid1 = (ptr + int_size - 1) / int_size * int_size;
  if (mid1 > end) {
    mid1 = end;
  }
  // get location of last word-aligned address at/before the end
  uint32_t mid3 = end / int_size * int_size;
  // get end location of optimized section
  uint32_t mid2 = mid1 + (mid3 - mid1) / (4 * int_size) * (4 * int_size);
  // create a word-sized integer
  uint32_t value = 0;
  for (uint16_t i = 0; i < int_size; ++i) {
    value <<= 8;
    value |= (uint8_t) x;
  }
  __ASM volatile (
  // store bytes
  "b Compare1%=\n"
  "Store1%=:\n"
  "strb %[value], [%[ptr]], #1\n"
  "Compare1%=:\n"
  "cmp %[ptr], %[mid1]\n"
  "bcc Store1%=\n"
  // store words optimized
  "b Compare2%=\n"
  "Store2%=:\n"
  "str %[value], [%[ptr]], #4\n"
  "str %[value], [%[ptr]], #4\n"
  "str %[value], [%[ptr]], #4\n"
  "str %[value], [%[ptr]], #4\n"
  "Compare2%=:\n"
  "cmp %[ptr], %[mid2]\n"
  "bcc Store2%=\n"
  // store words
  "b Compare3%=\n"
  "Store3%=:\n"
  "str %[value], [%[ptr]], #4\n"
  "Compare3%=:\n"
  "cmp %[ptr], %[mid3]\n"
  "bcc Store3%=\n"
  // store bytes
  "b Compare4%=\n"
  "Store4%=:\n"
  "strb %[value], [%[ptr]], #1\n"
  "Compare4%=:\n"
  "cmp %[ptr], %[end]\n"
  "bcc Store4%=\n"
  : // no outputs
  : [value] "r"(value),
  [ptr] "r"(ptr),
  [mid1] "r"(mid1),
  [mid2] "r"(mid2),
  [mid3] "r"(mid3),
  [end] "r"(end)
  );
  return base_ptr;
}

操作32kB数据时的速度差异:

  • 原始memset:197045个时钟周期(每字节约6个时钟周期)
  • 优化后的memset:22582个时钟周期(每字节约0.7个时钟周期)
  • 最大理论速度:16384个时钟周期

最大速度为每4个字节2个时钟周期(str指令的速度)。

原始的memset需要16个字节的代码。新的需要98个字节。


1

不使用-specs=nano.specs选项的链接。这将使用包含memset的C库版本,该版本针对速度进行了优化而不是大小。这将拉出许多其他函数的较大版本(通常是printfmalloc),可以通过其他链接器选项进行优化。检查反汇编和链接器映射文件会有所帮助。


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