这与 memset movq giving segfault 中的实现实验相同。 我一直在打印 memset 的结果,它似乎只打印出更改,而不打印字符串的其余部分。
experimentMemset: #memset(void *ptr, int value, size_t num)
movq %rdi, %rax #sets rax to the first pointer, to return later
.loop:
cmp $0, %edx #see if num has reached limit
je .end
movq %rsi, (%rdi) #copies value into rdi
inc %rdi #increments pointer to traverse string
subl $1, %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 内存,您将返回而不存储任何内容。
您可以通过将条件分支放在底部来使循环更加紧凑。您可以将声明更改为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 次时运行它。对于高速缓存中热的中型阵列,现代 CPU 使用 AVX 或 AVX512 存储可以将速度提高 32 到 64 倍。在具有 ERMSB 功能的 CPU 上,使用 rep stosb
字符串指令可以接近对齐缓冲区的速度。是的,x86 有一条实现 memset
的指令!
(或者对于更广泛的模式,wmemset
和 rep stosd
。在没有 ERMSB 但具有快速字符串的 CPU 上(PPro 以及 IvyBridge 之前的更高版本),rep stosd
或 stosq 更快,因此您可以 imul $0x01010101, %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 内核的现实生活中使用。
关于assembly - 我的 memset 实现结果仅打印更改,而不是整个结果字符串,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/60502624/