movq
хранит высокие нули в int value
, а не только младший байт. Это завершает строку C. И также записывает после конца ptr + длины, которую передает ваш вызывающий!
Используйте mov %sil, (%rdi)
для хранения 1 байта.
(Фактически вы храните 8 байтов с movq
, включая старшие 4 байта, которые согласно соглашению о вызовах могут содержать мусор, потому что они не являются частью 32-битного int value
. С этим вызывающим абонентом они также будут но ноль.)
Вы могли бы обнаружить это, изучив содержимое памяти с помощью отладчика или лучшего тестового жгута . Сделай это в следующий раз. Лучший вызывающий для отладки использовал бы write
или fwrite
для печати полного буфера, и вы могли бы передать это в hexdump -C
. Или просто используйте команду GDB x
для вывода байтов памяти.
Вы проверяете только %edx
, младшие 4 байта size_t num
в %rdx
. Если вызывающая программа попросит вас установить ровно 4 ГБ памяти, вы вернетесь без сохранения чего-либо.
Вы можете сделать l oop более компактным, поместив условную ветвь внизу. Вы можете изменить объявление на 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 / j cc из l oop чтобы пропустить проверку oop и превратить jmp
внизу в jcc
, который читает флаги, установленные dec
.
Эффективность
Конечно, хранение 1 байта за раз очень неэффективно, даже если мы оптимизируем l oop, чтобы большее количество процессоров могло запускать его с 1 итерацией за такт. Для средних массивов, горячих в кэше, современные процессоры могут go от 32 до 64 раз быстрее, используя хранилища AVX или AVX512. И может go приблизиться к этому быстрому для выровненных буферов со строковыми инструкциями rep stosb
на процессорах, которые имеют функцию ERMSB. Да, x86 имеет единственную инструкцию, которая реализует memset
!
(или для более широких шаблонов, wmemset
с rep stosd
. На процессорах без ERMSB, но с быстрыми строками (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. Реализации Glib c довольно умны и хороший пример.
Код ядра не может легко использовать FPU / SIMD, поэтому rep stos
memset и rep movsb
memcpy действительно используются в реальной жизни в Linux ядро.