Результат моей реализации memset печатает только изменения, а не всю строку результата - PullRequest
2 голосов
/ 03 марта 2020

Это тот же эксперимент по реализации из memset movq, дающий 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!

1 Ответ

2 голосов
/ 03 марта 2020

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 ядро.

...