2015-09-07 11 views
1

Вот небольшой фрагмент кода сборки (я использую синтаксис ассемблера gnu).Почему нажатие двойного на стеке с двумя 32-битными битами может быть намного медленнее, чем нажатие на него с помощью команд float (fldl & fstpl)?

.extern cos 
.section .data 
pi: .double 3.14 
.section .text 
.global slowcos 
.global fastcos 

fastcos: 
    fldl pi   
    subl $8, %esp # makes some space for a double on the stack 
    fstpl 0(%esp) # copy pi on top of the stack 
    call cos 
    addl $8, %esp 
    ret 

slowcos: 
    pushl pi+4  # push the last 4 bytes of pi on top of the stack 
    pushl pi  # push the first 4 bytes of pi on top of the stack 
    call cos 
    addl $8, %esp 
    retx 

Можно ссылаться на эти символ легко из C со следующими прототипами:

extern double fastcos(); 
extern double slowcos(); 

Они оба возвращают значение «сов (3.14)», но slowcos в два раза медленнее, чем fastcos на Intel 32 -бит архитектуры. Мой вопрос следующий:

Что может объяснить такое большое различие в производительности?

В Linux, вы можете проверить это, скопировав этот код в файл cos.asm вызова и вызова:

as --32 cos.asm -o cos.o 
gcc -m32 -O0 cos.o test.c -lm -o test 

(вы можете оставить --32/-m32 (должны), если вы? не на 64bits системы) где test.c является следующий исходный C файл:

#include <stdio.h> 
#include <time.h> 

#define N 40000000 

extern double fastcos(); 
extern double slowcos(); 

int main() { 
    int k; 
    double r; 
    clock_t t; 

    t = clock(); 
    for (k = 0; k < N;k ++) 
    r = fastcos(); 
    printf ("%gs\n",(double) (clock() - t)/CLOCKS_PER_SEC); 
    printf("fastcos = %g\n", r); 

    t = clock(); 
    for (k = 0; k < N;k ++) 
    r = slowcos(); 
    printf ("%gs\n",(double) (clock() - t)/CLOCKS_PER_SEC); 
    printf("slowcos = %g\n", r); 

    return 0; 
} 

на моем компьютере она выводит данные:

1.55687s 
fastcos = -0.999999 
2.29821s 
slowcos = -0.999999 

Еще одно замечание. Если вы добавите строку «.global id» в заголовки, замените строки «вызов cos» как fastcos, так и slowcos на «идентификатор вызова» и добавьте следующий «двойной идентификатор (double x) {return x;}» в C. Тогда вы получите:

0.360433s 
fastpi = 3.14 
0.370393s 
slowpi = 3.14 

Этот код должен потратить примерно в то же время за пределами внутреннего вызова функции соз (или идентификатор). Поэтому это должно указывать на то, что разница происходит во время выполнения функции cosinus. Но я не понимаю, что может оправдать такую ​​разницу. Нет разницы в выравнивании% esp.

Наконец, я хотел бы сказать, что я наблюдал эти различия в реальном «цифровом» коде, где узким местом часто является вычисление «элементарных математических функций» (например, cos или exp). Кроме того, обе версии создаются компилятором языка программирования высокого уровня. Моя главная задача - понять, что там происходит.

+1

Экспедирование грузов-грузов. – EOF

+1

Хм, довольно уверен, что мой компилятор поднимет вызов cos() из цикла, а затем полностью исключит цикл. Таким образом, вы, вероятно, не используете оптимизированную сборку. Откуда вы знаете, что это не цикл for(), вызвавший замедление? Например, цель ветви не будет выровнена. Попробуйте поменять вызовы на slow/fastcos(). –

+1

@HansPassant: компилятор не разбирает сборку, все, что ей нужно, это прототип функции. Без '__attribute __ ((const))' или что-то подобное, компилятор не может знать, что каждый вызов 'fastcos()' делает то же самое. Однако вы можете быть на что-то с выравниванием функций. – EOF

ответ

7

Когда современный x86 пишет в память, и та же память считывается снова вскоре после этого, он читы делать, чтобы избежать полного туда-обратно в память/кэш:

Intel® 64 и IA -32 архитектуры Optimization Справочное руководство

2.3.4.4 магазин Forwarding

Если нагрузка следует хранить и перезагружает данные о том, что магазин записывает в памяти, Intel Core микроархитектуры может переадресовать д ata непосредственно от магазина до груза. Этот процесс, называемый хранилищем до пересылкой груза, экономит циклы, позволяя нагрузке получать данные непосредственно из операции магазина, а не через память.

Текст идет о требованиях выравнивания, но главное заключается в следующем:

Магазин должен быть равен или больше по размеру, чем размер загружаемых данных.

В медленной функции вы храните восьмибайтовый двойной в двух кусках четырех байтов. Предположительно, функция cos() загружает ее в один кусок, поэтому нагрузка должна ждать, пока хранилище не будет привязано к кешу.

С другой стороны, в быстрой функции вы храните один восьмибайтовый кусок, который остается во внутренних буферах процессора, откуда можно сразу же загрузить нагрузку cos().