Почему GCC вставляет на первый взгляд несущественные инструкции перед вызовом printf? - PullRequest
1 голос
/ 30 мая 2011

Я пытаюсь изучать x86 самостоятельно и решил проанализировать простую программу на c и посмотреть, что выводит GCC.Программа выглядит так:

#include <stdio.h>
int main() {
  printf("%s","Hello World");
  return 0;
}

Я скомпилировал код с -S, а затем удалил то, что мне показалось ненужным, и уменьшил код сборки до этого.

.pfArg:
.string "%s"
.text

.Hello:
.string "Hello World"
.text

.globl main
.type   main, @function

main:
pushq   %rbp        # push what was in base pointer onto stack
movq    %rsp, %rbp  # move stack pointer to base pointer
subq    $16, %rsp   # subtract 16 from sp and store in stack pointer

# prepare arguments for printf
movl    $.Hello, %esi   # put & of "Hello World" into %esi
movq    $.pfArg, %rdi   # put & of "%d" into %eax
call    printf
leave
ret

Теперь почти всев приведенном выше коде имеет смысл для меня, кроме первых двух под основной.Хотя это то, что я получаю, не разбирая вещи.

.LC0:
    .string "%s"

.LC1:
    .string "Hello World"
    .text

.globl main
    .type   main, @function

main:

.LFB0:
    pushq   %rbp        # push what was in base pointer onto stack
    movq    %rsp, %rbp  # move stack pointer to base pointer

  # prepare arguments for printf
    movl    $.LC0, %eax # put arg into %eax
    movl    $.LC1, %esi # put second arg into %esi
    movq    %rax, %rdi  # move value in %rax to %rdi ???? ( why not just put $.LCO into %rax directly )
    movl    $0, %eax    # clear out %eax ???? ( why do we need to clear it out )
    call    printf      
    movl    $0, %eax    # return 0
    leave
    ret

.LFE0:
    .size   main, .-main
    .ident  "GCC: (Ubuntu/Linaro 4.5.2-8ubuntu4) 4.5.2"
    .section    .note.GNU-stack,"",@progbits

Есть 2 инструкции, которые я пометил ????что я не понимаю

Первая инструкция перемещает содержимое% rax в% rdi для подготовки к вызову printf.Это нормально, за исключением того, что мы только что переместили $ .LC0 (это строка "% s") в% eax.Это кажется ненужным, почему мы просто не переместили $ .LC0 в% rdi, а не переместили его в% eax, а затем в% rdi?

Вторая инструкция очищает% eax, который я понимаюбыть возвращаемым значением функции.Но если в любом случае функция просто закроет ее, почему GCC заботится об ее очистке?

Ответы [ 4 ]

6 голосов
/ 31 мая 2011

Эмпирические правила:

  1. Не беспокойтесь о неоптимизированном выводе, если вы заинтересованы в эффективном коде.
  2. Всегда измеряйте, никогда не думайте, что ваши «улучшения» на уровне ассемблера повышают производительность.

Даже в оптимизированном коде вы можете увидеть, казалось бы, ненужные инструкции, такие как «xor% eax,% eax», когда нет функциональной необходимости в засорении регистра. Эти инструкции играют особую роль, сообщая конвейеру, что за этой точкой не существует зависимости данных для этого регистра. В современном неработающем процессоре конвейер ядра спекулятивно выполняет много инструкций перед текущим EIP. Явное сокращение зависимости данных таким образом помогает спекулятивному механизму и может повысить производительность, особенно в тесных циклах.

В других случаях, по-видимому, компилятор может использовать обходной подход, когда фактически пытается сопоставить выполняемую работу с параллельными исполнительными блоками, доступными в конвейере целевого ядра. Больше команд, отправляемых параллельно, часто выполняется быстрее, чем меньшее количество сериализованных команд.

Если вы действительно хотите выжать каждую последнюю потерю производительности, используйте инструкцию rdtsc до и после блока кода, чтобы измерить количество затраченных часов. Будьте немного осторожны, поскольку rdtsc строго не упорядочен с окружающими инструкциями, но на практике измерения достаточно точны для всего, что находится в диапазоне 1000-х часов.

4 голосов
/ 30 мая 2011

Вы просматриваете оптимизированный вывод или неоптимизированный (что по сути является наивным переводом кода C на ассемблер)?Это имеет огромное значение, так как оптимизатор обычно довольно хорошо применяет те же правила, которые вы описываете.

3 голосов
/ 07 июля 2011

Первая инструкция перемещает то, что в %rax, в %rdi, чтобы подготовиться к вызову printf. Все в порядке, за исключением того, что мы только что переместили $.LC0 (это строка "%s") в %eax. Это кажется ненужным, почему мы просто не переместили $.LC0 в %rdi в первую очередь вместо того, чтобы переместить его в %eax, а затем в %rdi?

Это возможно потому, что вы компилируете без оптимизаций. Когда я компилирую ваш пример с GCC 4.2.1 на Mac OS X v10.6.8, я получаю следующий вывод:

.globl _main
_main:
LFB3:
    pushq   %rbp
LCFI0:
    movq    %rsp, %rbp
LCFI1:
    leaq    LC0(%rip), %rsi
    leaq    LC1(%rip), %rdi
    movl    $0, %eax
    call    _printf
    movl    $0, %eax
    leave
    ret

Как видите, аргументы были напрямую сохранены в %rsi и %rdi.

Вторая инструкция очищает %eax, что, как я понимаю, является возвращаемым значением функции. Но если в любом случае функция просто закроет ее, почему GCC хочет ее очистить?

Поскольку ABI x86_64 указывает, что если функция принимает переменные аргументы, то AL (который является частью %eax), как ожидается, будет содержать число векторных регистров, используемых для аргументов этого вызова функции. Поскольку вы не указываете аргументы с плавающей точкой при вызове printf(), векторные регистры не используются, поэтому AL (%eax) обнуляется. Я привожу больше примеров в ответе на другой вопрос здесь .

2 голосов
/ 30 мая 2011

Поскольку GCC является компилятором, а компиляторы глупы.

Вы можете сделать GCC умнее, используя -O2.Он начинает использовать приемы оптимизации и уменьшает избыточные инструкции.

...