Почему толкание двойного стека с помощью двух 32-битных толчков может быть намного медленнее, чем его использование с помощью инструкций с плавающей запятой (fldl & fstpl)?
Вот небольшой фрагмент ассемблерного кода (я использую синтаксис ассемблера gnu).
.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 ();
Они оба возвращают значение cos(3.14), но slowcos в два раза медленнее, чем fastcos на 32-битной архитектуре Intel. Мой вопрос заключается в следующем:
Что может объяснить такую большую разницу в производительности?
В linux вы можете проверить это, скопировав этот код в вызове файла cos.asm и вызвав:
as --32 cos.asm -o cos.o
gcc -m32 -O0 cos.o test.c -lm -o test
(вы можете удалить --32/-m32 (должен?), если вы не в 64-битной системе), где 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" в заголовки, замените строки "call cos" в fastcos и slowcos на "call id" и добавьте следующий "double id (double x) { return x; }" в С файл. Затем вы получите:
0.360433s
fastpi = 3.14
0.370393s
slowpi = 3.14
Этот код должен проводить примерно то же время за пределами внутреннего вызова функции cos (или id). Таким образом, это должно указывать на то, что разница происходит во время выполнения функции косинуса. Но я не понимаю, что может оправдать такую разницу. Нет никакой разницы в выравнивании% esp.
Наконец, я хотел бы сказать, что я наблюдал эти различия в реальном "числовом" коде, где узким местом часто является вычисление "элементарных математических функций" (например, cos или exp). Также обе версии выпускаются компилятором языка программирования высокого уровня. Моя главная задача - понять, что там происходит.
1 ответ
Когда современный x86 записывает в память, и вскоре после этого снова читается та же самая память, он обманывает, чтобы избежать полного обхода памяти / кеша:
Справочное руководство по оптимизации архитектур Intel® 64 и IA-32
2.3.4.4. Экспедирование в магазине
Если загрузка следует за хранилищем и перезагружает данные, которые хранилище записывает в память, микроархитектура Intel Core может пересылать данные непосредственно из хранилища в загрузку. Этот процесс, называемый сохранением для пересылки загрузки, сохраняет циклы, позволяя загрузке получать данные непосредственно из операции сохранения, а не через память.
Продолжается текст о требованиях к выравниванию, но важно следующее:
Размер хранилища должен быть больше или равен размеру загружаемых данных.
В медленной функции вы сохраняете восьмибайтовый дубль в двух блоках по четыре байта. Предположительно, cos()
-функция загружает его в один блок, поэтому загрузка должна ждать, пока хранилище не будет передано в кэш.
С другой стороны, в быстрой функции вы сохраняете один восьмибайтовый фрагмент, который остается во внутренних буферах процессора, откуда загружается cos()
могут быть удовлетворены сразу.