我实现 memset 的结果只打印更改,而不是整个结果字符串
Posted
技术标签:
【中文标题】我实现 memset 的结果只打印更改,而不是整个结果字符串【英文标题】:Result of my implementation of memset only prints the changes, and not the entire result string 【发布时间】:2020-03-03 08:03:48 【问题描述】:这是来自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 %rsi, (%rdi)
写入 8 个字节,而不是您想要的 1 个字节。
你可能想要movb %sil, (%rdi)
【参考方案1】:
movq
在int value
中存储高零,而不仅仅是低字节。这会终止 C 字符串。 并且还会写到调用者传递的 ptr+length 的末尾!
使用mov %sil, (%rdi)
存储1个字节。
(实际上,您使用 movq
存储 8 个字节,包括根据调用约定允许包含垃圾的高 4 个字节,因为它们不是 32 位 int value
的一部分。有了这个不过,调用者它们也将为零。)
您可以通过使用调试器或更好的测试工具检查内存内容来检测到这一点。下次这样做。更好的调试调用者会使用write
或fwrite
来打印完整的缓冲区,您可以将其通过管道传输到hexdump -C
。或者只是使用 GDB 的 x
命令转储内存字节。
您只检查%edx
,size_t num
的低 4 字节在%rdx
。如果您的调用者要求您准确设置 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 内核中用于现实生活。
【讨论】:
以上是关于我实现 memset 的结果只打印更改,而不是整个结果字符串的主要内容,如果未能解决你的问题,请参考以下文章