我执行 memset 的结果只打印更改,而不是整个结果字符串
Result of my implementation of memset only prints the changes, and not the entire result string
这与 中的实施实验相同
我一直在打印出 memset 的结果,它似乎只打印出更改,而不是字符串的其余部分。
experimentMemset: #memset(void *ptr, int value, size_t num)
movq %rdi, %rax #sets rax to the first pointer, to return later
.loop:
cmp [=10=], %edx #see if num has reached limit
je .end
movq %rsi, (%rdi) #copies value into rdi
inc %rdi #increments pointer to traverse string
subl , %edx #decrements count
jmp .loop
.end:
ret
int main {
char str[] = "almost every programmer should know memset!";
printf("MEMSET\n");
my_memset(str, '-', 6);
printf("%s\n", str);
}
我的输出:------
cplusplus.com 的正确输出:------ 每个程序员都应该知道 memset!
movq
将高位零存储在 int value
中,而不仅仅是低字节。这将终止 C 字符串。 并且还会写入你的调用者传递的 ptr+length 的末尾!
使用mov %sil, (%rdi)
存储1个字节。
(实际上,您使用 movq
存储了 8 个字节,包括根据调用约定允许包含垃圾的高 4 个字节,因为它们不是 32 位 int value
。对于这个调用者,它们也将为零。)
您可以通过使用调试器或更好的测试工具检查内存内容来检测到这一点。下次再做。更好的调试调用程序会使用 write
或 fwrite
来打印完整的缓冲区,您可以将其通过管道传输到 hexdump -C
。或者只使用 GDB 的 x
命令来转储内存字节。
你只检查%edx
,%rdx
中size_t num
的低4字节。如果您的来电者要求您设置正好 4GiB 的内存,您将 return 不存储任何内容。
您可以通过将条件分支放在底部来使循环更紧凑。您可以将声明更改为 unsigned num
,或者您可以修复您的代码。
.globl experimentMemset
experimentMemset: #memset(void *ptr, int value, size_t num)
movq %rdi, %rax #sets rax to the first pointer, to return later
test %rdx, %rdx # special case: size = 0, loop runs zero times
jz .Lend
.Lloop: # do{
mov %sil, (%rdi) # store the low byte of int value
inc %rdi # ++ptr
dec %rdx
jnz .Lloop # }while(--count);
.Lend:
ret
它甚至没有更多的指令:我只是将 cmp/jcc 从循环中拉出来,使其成为一个跳过循环检查,并将底部的 jmp
变成 jcc
读取由 dec
.
设置的标志
效率
当然,一次存储 1 个字节是非常低效的,即使我们优化循环以使更多 CPU 可以 运行 它在每个时钟迭代 1 次。对于高速缓存中的中型阵列,使用 AVX 或 AVX512 存储,现代 CPU 的速度可以提高 32 到 64 倍。在具有 ERMSB 功能的 CPU 上,对于使用 rep stosb
字符串指令对齐的缓冲区,可以接近那么快。是的,x86 有一条实现 memset
!
的指令
(或者对于更宽的模式,wmemset
和 rep stosd
。在没有 ERMSB 但具有快速字符串(PPro 和 IvyBridge 之前的更高版本)的 CPU 上,rep stosd
或 stosq 更快,所以你可能 imul [=33=]x01010101, %esi, %eax
广播低字节。)
# slowish for small or misaligned buffers
# but probably still better than a byte loop for buffers larger than maybe 16 bytes
.globl memset_ermsb
memset_ermsb: #memset(void *ptr, int value, size_t num)
mov %rdx, %rcx # count = num
mov %esi, %eax # AL = char to set
rep stosb # destination = RDI
ret
真正的 memset 实现使用 SIMD 循环,因为这对于小的或未对齐的缓冲区来说更快。关于优化 memset / memcpy 的文章很多。 Glibc 的实现非常聪明,是一个很好的例子。
内核代码无法轻松使用 FPU/SIMD,因此 rep stos
memset 和 rep movsb
memcpy 确实在 Linux 内核中得到了实际使用。
这与
experimentMemset: #memset(void *ptr, int value, size_t num)
movq %rdi, %rax #sets rax to the first pointer, to return later
.loop:
cmp [=10=], %edx #see if num has reached limit
je .end
movq %rsi, (%rdi) #copies value into rdi
inc %rdi #increments pointer to traverse string
subl , %edx #decrements count
jmp .loop
.end:
ret
int main {
char str[] = "almost every programmer should know memset!";
printf("MEMSET\n");
my_memset(str, '-', 6);
printf("%s\n", str);
}
我的输出:------
cplusplus.com 的正确输出:------ 每个程序员都应该知道 memset!
movq
将高位零存储在 int value
中,而不仅仅是低字节。这将终止 C 字符串。 并且还会写入你的调用者传递的 ptr+length 的末尾!
使用mov %sil, (%rdi)
存储1个字节。
(实际上,您使用 movq
存储了 8 个字节,包括根据调用约定允许包含垃圾的高 4 个字节,因为它们不是 32 位 int value
。对于这个调用者,它们也将为零。)
您可以通过使用调试器或更好的测试工具检查内存内容来检测到这一点。下次再做。更好的调试调用程序会使用 write
或 fwrite
来打印完整的缓冲区,您可以将其通过管道传输到 hexdump -C
。或者只使用 GDB 的 x
命令来转储内存字节。
你只检查%edx
,%rdx
中size_t num
的低4字节。如果您的来电者要求您设置正好 4GiB 的内存,您将 return 不存储任何内容。
您可以通过将条件分支放在底部来使循环更紧凑。您可以将声明更改为 unsigned num
,或者您可以修复您的代码。
.globl experimentMemset
experimentMemset: #memset(void *ptr, int value, size_t num)
movq %rdi, %rax #sets rax to the first pointer, to return later
test %rdx, %rdx # special case: size = 0, loop runs zero times
jz .Lend
.Lloop: # do{
mov %sil, (%rdi) # store the low byte of int value
inc %rdi # ++ptr
dec %rdx
jnz .Lloop # }while(--count);
.Lend:
ret
它甚至没有更多的指令:我只是将 cmp/jcc 从循环中拉出来,使其成为一个跳过循环检查,并将底部的 jmp
变成 jcc
读取由 dec
.
效率
当然,一次存储 1 个字节是非常低效的,即使我们优化循环以使更多 CPU 可以 运行 它在每个时钟迭代 1 次。对于高速缓存中的中型阵列,使用 AVX 或 AVX512 存储,现代 CPU 的速度可以提高 32 到 64 倍。在具有 ERMSB 功能的 CPU 上,对于使用 rep stosb
字符串指令对齐的缓冲区,可以接近那么快。是的,x86 有一条实现 memset
!
(或者对于更宽的模式,wmemset
和 rep stosd
。在没有 ERMSB 但具有快速字符串(PPro 和 IvyBridge 之前的更高版本)的 CPU 上,rep stosd
或 stosq 更快,所以你可能 imul [=33=]x01010101, %esi, %eax
广播低字节。)
# slowish for small or misaligned buffers
# but probably still better than a byte loop for buffers larger than maybe 16 bytes
.globl memset_ermsb
memset_ermsb: #memset(void *ptr, int value, size_t num)
mov %rdx, %rcx # count = num
mov %esi, %eax # AL = char to set
rep stosb # destination = RDI
ret
真正的 memset 实现使用 SIMD 循环,因为这对于小的或未对齐的缓冲区来说更快。关于优化 memset / memcpy 的文章很多。 Glibc 的实现非常聪明,是一个很好的例子。
内核代码无法轻松使用 FPU/SIMD,因此 rep stos
memset 和 rep movsb
memcpy 确实在 Linux 内核中得到了实际使用。