Почему функция кода asm в C, занимает больше времени, чем функция кода c? - PullRequest
0 голосов
/ 24 февраля 2019

Я написал простую функцию умножения на C, а другую - на ассемблере, используя ключевое слово "asm" GCC.

Я взял время выполнения для каждого из них, и хотя их время довольно близко,Функция C немного быстрее, чем в ассемблерном коде.

Я хотел бы знать, почему, поскольку я ожидал, что ассемблер будет быстрее.Это из-за дополнительного «вызова» (я не знаю, какое слово использовать) к ключевому слову «asm» в GCC?

Вот функция C:

int multiply (int a, int b){return a*b;}

А вот asm в файле C:

int asmMultiply(int a, int b){  
    asm ("imull %1,%0;"
             : "+r" (a)           
             : "r" (b)
    );
    return a;
}

мой главный, где я беру время:

int main(){
   int n = 50000;
   clock_t asmClock = clock();
   while(n>0){
       asmMultiply(4,5);
       n--;
    }

   asmClock = clock() - asmClock;  
   double asmTime = ((double)asmClock)/CLOCKS_PER_SEC; 

   clock_t cClock = clock();
   n = 50000;
   while(n>0){
       multiply(4,5);
       n--;
   }
   cClock = clock() - cClock;  
   double cTime = ((double)cClock)/CLOCKS_PER_SEC;  

  printf("Asm time: %f\n",asmTime);
  printf("C code time: %f\n",cTime);

Спасибо!

Ответы [ 2 ]

0 голосов
/ 25 февраля 2019

Эта попытка микробенчмарка слишком наивна почти во всех возможных случаях, чтобы вы могли получить какие-либо значимые результаты.

Даже если вы исправили поверхностные проблемы (поэтому код не оптимизировался)прочь), существуют серьезные проблемы, прежде чем вы сможете сделать вывод о том, когда ваш asm будет лучше, чем *.

(Подсказка: , вероятно, никогда . Компиляторы уже знают, какОптимальное умножение целых чисел и понимание семантики этой операции. Принудительное использование imul вместо автоматической векторизации или других оптимизаций приведет к потерям.)


Оба рассчитанырегионы пусты, потому что оба умножения могут оптимизировать прочь .(asm - это не asm volatile, и вы не используете результат.) Вы измеряете только шум и / или увеличение частоты процессора до максимального значения turbo до clock().

И даже если бы этого не было, одна imul инструкция в основном неизмерима с функцией с такими дополнительными издержками, как clock().Возможно, если вы сериализовали с lfence, чтобы заставить процессор ждать imul для выхода из системы, прежде чем rdtsc ... См. RDTSCP в NASM всегда возвращает одно и то же значение

ИлиВы скомпилировали с отключенной оптимизацией, что бессмысленно.


Вы не можете измерить оператор C * и встроенный asm без некоторого контекста, включающего цикл .И тогда это будет для этого контекста , в зависимости от того, какие оптимизации вы победили, используя встроенный asm.(И что если вы сделаете все, чтобы компилятор не оптимизировал работу для чистой версии C).

Измерение только одного числа для одной инструкции x86 мало о чем вам говорит.Вам необходимо измерить задержку, пропускную способность и стоимость переднего плана, чтобы правильно охарактеризовать ее стоимость.Современные x86-процессоры являются суперскалярными конвейерами с неупорядоченным порядком, поэтому сумма затрат на 2 инструкции зависит от того, зависят ли они друг от друга, и от другого окружающего контекста. Сколько циклов ЦП необходимо для каждой инструкции по сборке?


Автономные определения функций идентичны после вашего изменения, чтобы позволить компиляторувыберите регистры, и ваш asm может встраиваться несколько эффективнее, но это все еще не помогает оптимизации.gcc знает, что 5 * 4 = 20 во время компиляции, поэтому, если бы вы использовали результат, multiply(4,5) мог бы оптимизироваться до немедленного 20.Но gcc не знает, что делает asm, поэтому ему просто нужно ввести входные данные хотя бы один раз.(не volatile означает, что он может CSE результат, если вы использовали asmMultiply(4,5) в цикле.)

Таким образом, среди прочего, inline asm предотвращает постоянное распространение.Это имеет значение, даже если только один из входных данных является константой, а другой - переменной времени выполнения.Многие малые целочисленные умножители могут быть реализованы с помощью одной или двух инструкций LEA или сдвига (с меньшей задержкой, чем 3c для imul на современном x86).

https://gcc.gnu.org/wiki/DontUseInlineAsm

Единственный вариант использования, который я мог бы себе представить asm, помогает, если компилятор использовал 2x инструкции LEA в ситуации, которая на самом деле связана с внешним интерфейсом, где imul $constant, %[src], %[dst] позволил бы копировать и умножать с 1 моп вместо 2. Новаш asm исключает возможность использования немедленных (вы только допустили ограничения регистра), а встроенный GNU C не может позволить вам использовать другой шаблон для непосредственного сравнения с регистром.Может быть, если вы использовали множественные альтернативные ограничения и соответствующее ограничение регистра для части только для регистра?Но нет, вам все равно нужно иметь что-то вроде asm("%2, %1, %0" :...), и это не может работать для рег, рег.

. Вы можете использовать if(__builtin_constant_p(a)) { asm using imul-immediate } else { return a*b; }, который будет работать с GCC, чтобы позволить вам победить LEA.Или, в любом случае, просто потребуйте постоянного множителя, так как вы захотите использовать его только для конкретной версии gcc, чтобы обойти конкретную пропущенную оптимизацию.( то есть это настолько ниша, что на практике вы никогда бы этого не сделали. )


Ваш код в проводнике компилятора Godbolt , с clang7.0 -O3 для x86-64 Соглашения о вызовах System V:

# clang7.0 -O3   (The functions both inline and optimize away)
main:                                   # @main
    push    rbx
    sub     rsp, 16
    call    clock
    mov     rbx, rax                 # save the return value
    call    clock
    sub     rax, rbx                 # end - start time
    cvtsi2sd        xmm0, rax
    divsd   xmm0, qword ptr [rip + .LCPI2_0]
    movsd   qword ptr [rsp + 8], xmm0 # 8-byte Spill


    call    clock
    mov     rbx, rax
    call    clock
    sub     rax, rbx             # same block again for the 2nd group.

    xorps   xmm0, xmm0
    cvtsi2sd        xmm0, rax
    divsd   xmm0, qword ptr [rip + .LCPI2_0]
    movsd   qword ptr [rsp], xmm0   # 8-byte Spill
    mov     edi, offset .L.str
    mov     al, 1
    movsd   xmm0, qword ptr [rsp + 8] # 8-byte Reload
    call    printf
    mov     edi, offset .L.str.1
    mov     al, 1
    movsd   xmm0, qword ptr [rsp]   # 8-byte Reload
    call    printf
    xor     eax, eax
    add     rsp, 16
    pop     rbx
    ret

TL: DR: если вы хотите понять производительность встроенного asm на этом детализированном уровне детализации, вам необходимо понять, как компиляторы оптимизируют в первую очередь.

0 голосов
/ 24 февраля 2019

Функция сборки выполняет больше работы, чем функция C, - она ​​инициализирует mult, затем выполняет умножение и присваивает результат mult, а затем помещает значение из mult в место возврата.

Компиляторы хороши в оптимизации;Вы не сможете легко победить их в базовой арифметике.

Если вы действительно хотите улучшения, используйте static inline int multiply(int a, int b) { return a * b; }.Или просто введите a * b (или эквивалент) в коде вызова вместо int x = multiply(a, b);.

...