Встроенная сборка против математической библиотеки

Высокий, может ли кто-нибудь помочь мне понять, почему более эффективно вызывать функции библиотеки математики, чем писать код встроенной сборки для выполнения той же операции? Я написал этот простой тест:

#include <stdio.h>
#define __USE_GNU
#include <math.h>

void main( void ){
    float ang;
    int i;

    for( i = 0; i< 1000000; i++){
        ang = M_PI_2 * i/2000000;
    /*__asm__ ( "fld %0;"
              "fptan;"
              "fxch;"
              "fstp %0;" : "=m" (ang) : "m" (ang)
    ) ;*/
    ang = tanf(ang);
    }
    printf("Tan(ang): %f\n", ang);
}

Этот код вычисляет тангенс угла двумя различными способами: один вызывает функцию tanf из динамически связанной библиотеки libm.a, а второй - с использованием встроенного кода сборки. Обратите внимание, что я комментирую части кода поочередно. Код выполняет операцию несколько раз, чтобы получить значимые результаты с помощью команды time y linux Terminal.

Версия, в которой используется математическая библиотека, занимает около 0,040 с. Версия, которая использует код сборки, занимает около 0,440 с; в десять раз больше.

Это результаты разборки. Оба были скомпилированы с опцией -O3.

LIBM

4005ad: b8 db 0f c9 3f          mov    $0x3fc90fdb,%eax
  4005b2:   89 45 f8                mov    %eax,-0x8(%rbp)
  4005b5:   f3 0f 10 45 f8          movss  -0x8(%rbp),%xmm0
  4005ba:   e8 e1 fe ff ff          callq  4004a0 <tanf@plt>
  4005bf:   f3 0f 11 45 f8          movss  %xmm0,-0x8(%rbp)
  4005c4:   83 45 fc 01             addl   $0x1,-0x4(%rbp)
  4005c8:   83 7d fc 00             cmpl   $0x0,-0x4(%rbp)
  4005cc:   7e df                   jle    4005ad <main+0x19>

КАК М

40050d: b8 db 0f c9 3f          mov    $0x3fc90fdb,%eax
  400512:   89 45 f8                mov    %eax,-0x8(%rbp)
  400515:   d9 45 f8                flds   -0x8(%rbp)
  400518:   d9 f2                   fptan  
  40051a:   d9 c9                   fxch   %st(1)
  40051c:   d9 5d f8                fstps  -0x8(%rbp)
  40051f:   83 45 fc 01             addl   $0x1,-0x4(%rbp)
  400523:   83 7d fc 00             cmpl   $0x0,-0x4(%rbp)
  400527:   7e e4                   jle    40050d <main+0x19>

Любая идея? Благодарю.

Я думаю, у меня есть идея. Просматривая код glibc, я обнаружил, что функция tanf реализована с помощью полиномиального приближения и с использованием расширения sse. Я полагаю, что это быстрее, чем микрокод для инструкции fptan.

2 ответа

Существует большая разница в реализации этих функций.

fptan является устаревшей инструкцией 8087, использующей стек с плавающей запятой. Даже изначально инструкции 8087 были микрокодированы. Вызов fptan инструкция вызвала запуск предопределенной программы в процессоре 8087, который использовал бы основные возможности процессора, такие как сложение с плавающей запятой или даже умножение. Микрокодирование обходит некоторые этапы "естественной" конвейерной обработки, например, предварительную выборку и декодирование, и это ускоряет процесс.

Алгоритм, выбранный для тригонометрических функций в 8087 году, был CORDIC.

Несмотря на то, что микрокодирование делает fptan быстрее, чем явный вызов каждой инструкции, это не было концом разработки процессора с плавающей запятой; Можно сказать, что разработка 8087 г. закончена. В будущих процессорах fptan, вероятно, должен быть реализован как есть, как IP-блок, который ведет себя идентично исходной инструкции с некоторой связующей логикой для получения побитового точного вывода как оригинала.

Позднее процессоры сначала переработали стек FP для "MMX". Затем был введен совершенно новый набор регистров (XMM) вместе с набором команд (SSE), способным к параллельному выполнению основных операций с плавающей запятой. Во-первых, поддержка плавающей запятой с расширенной точностью (80 бит) была прекращена. С другой стороны, 20-летний закон Мура позволил выделить гораздо более высокое число транзисторов, например, для наращивания, например, 64x64-битных параллельных умножителей, увеличивающих пропускную способность умножения.

Другие инструкции тоже пострадали: loop когда-то был быстрее, чем sub ecx, 1; jnz сочетание. aam сегодня, вероятно, медленнее, чем условное добавление 10 к какому-то куску eax - эти 20 с лишним лет закона Мура позволили миллионам транзисторов ускорить стадию предварительной выборки: в 8086 году каждый байт в кодировке команд считался еще одним циклом. Сегодня несколько инструкций выполняются в течение одного цикла, потому что инструкция уже извлечена из памяти.

При этом, вы также можете попробовать, если одна инструкция, такая как aam на самом деле быстрее, чем реализация его содержимого с использованием эквивалентного набора более простых инструкций, которые оптимизированы. Это преимущество библиотеки: они могут использовать инструкцию fptan, но они не нужны, если архитектура процессора поддерживает более быстрый набор инструкций, больший параллелизм, более быстрый алгоритм или все эти.

Здесь (Fedora 20, gcc-4.8.2-7.fc20.x86_64, процессор Intel® Core® TM i7-2670QM @ 2,20 ГГц, скомпилированный с -O2) я вижу время пользователя 0,161 с (asm) против 0,076 с (libm).

И хотя компилятору будет позволено избавиться от цикла в версии библиотеки (он знает, tanf(3m) это чистая функция), сборка показывает, что цикл есть. И функция не встроена, это вызов функции здесь. Еще быстрее Странный.

Хорошо, похоже, разница в том, чтобы перетаскивать аргумент asm() фрагмент кода (он помещается в локальную переменную и используется оттуда). Я не эксперт по x86_64, и мои ограничения GCC ASM-фу проржавели...

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

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