Почему толкание двойного стека с помощью двух 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() могут быть удовлетворены сразу.

Другие вопросы по тегам