2016-09-13 4 views
0

Я читал о математическом сопроцессоре (Paul Carters PC Assembly Book) и его инструкциях для расчета с плавающей запятой (на ASM i386). Тогда я побежал в следующий код, который должен вернуть большой двойник двух заданных двойных значений (C Вызов Конвенции):Сборка (i386): Math Сопроцессорный стек

1 %define d1 ebp+8 
2 %define d2 ebp+16 
3 global dmax 
4  
5 segment .text 
6 dmax: 
7  enter 0,0 
8  
9  fld qword [d2] 
10  fld qword [d1] ;Now ST0 = d1 and ST1 = d2 
11  fcomip st1 ;Compares ST0 with ST1 and pops ST0 out 
12  jna short d2_bigger ;If not above (ST0<ST1) 
13  fcomp st0 ;Get rid of ST0, which is actually d2 now (line 11) 
14  fld qword [d1] 
15  jmp short exit 
16 d2_bigger: 
17 exit: 
18  leave 
19  ret 

Существовало две вещей, я думал об изменении этого кода. Во-первых, я бы использовал FCOMI вместо FCOMIP для сравнения (строка 11), чтобы избежать 1 ненужного всплывающего регистра сопроцессора. Сделав это, если ST0 = ST1 вообще не будет всплывать (поскольку он уже находится в верхней части стека). Единственная причина, по которой я не могу этого сделать, это то, что оставил бы непустую стек регистров сопроцессора. Однако, я думаю, единственное релевантное значение для C - ST0, которое будет возвратным значением двойной функции. Если другая функция переместила более 8 значений float/double в стек сопроцессора, не будут ли отброшены значения, хранящиеся в младших членах стека сопроцессора (ST7)? Итак, действительно ли проблема оставить функцию без очистки стека сопроцессора? => (READ EDIT)

Второе, что я имел в виду изменения, я бы, вероятно, не использовать инструкцию FCOMP на линии 13. Я понимаю причина, это есть поп ST0 из стека, чтобы ST1 достигла вершины. Тем не менее, я думаю, что это немного накладные расходы, чтобы провести полное сравнение и установить флаги сопроцессора, чтобы вывести значение. Я искал инструкцию только для ввода ST0 и, видимо, ее нет. Я думал, что будет быстрее, хотя использовать FADDP ST0, ST0 (добавляет ST0 в ST0 и выталкивает ST0) или FSTP ST0 (сохраняет значение ST0 до ST0 и выдает ST0 вне). Они просто выглядят в моей голове, как меньше работы для сопроцессора.

Я попытался проверить скорость 3-х опций (один на код выше, FSTP ST0 и FADDP ST0, ST0), и после нескольких быстрых тестов все они работали с очень близкими скоростями. Вид неточно сделать вывод из ценностей. По-видимому,FADDP ST0,ST0 был немного быстрее, затем FSTP ST0 и, наконец, FCOMP ST0. Есть ли рекомендация по использованию? Или я слишком много беспокоюсь о чем-то, что будет иметь такой незначительный эффект на общую скорость?

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


EDIT:

Я читал Intel 64 и IA-32 Набор команд и, по-видимому сопроцессор бросает исключение, если переполнение стека или недорасход (Exception #IS). Таким образом, используя стек и не опорожняя его (в этом случае, оставляя только ST0, чтобы C выдавал его возвращаемое значение), по-видимому, это не вариант.

+2

Мир быстро иссякает машины, где этот вид код все еще имеет смысл. Особенно, когда вы используете сборку. Вместо этого используйте код SSE2. Используйте недавний компилятор C, если вы не знаете, как это выглядит. –

+0

@ HansPassant IIRC NASA некоторое время держал на своих компьютерах 386-486 процессоров. С их большими транзисторами они менее уязвимы для космических лучей, изменяющих некоторую битовую ценность. Но это несколько лет назад, я понятия не имею, что такое текущее состояние. :) В другом месте это, вероятно, так, как вы его написали, SSE2 и более доступны. – Ped7g

+0

@ Ped7g: используется FCOMI, который доступен только на p6. –

ответ

3

Современные процессоры обрабатывают операции стека регистров x87, аналогичные тем, как они выполняют переименование регистров для выполнения вне очереди. P-версии команд x87 выполняются с теми же характеристиками производительности, что и не поп-версии.

Для всего, что вам нужно для статистического анализа латентности, пропускной способности и общего количества ошибок для этого кода на современных процессорах, см. Agner Fog's microarch guide and instruction tables.Также, tag wiki for more links.

О, и, безусловно, никогда не используйте инструкцию ВВОД, если полностью не оптимизируйте размер, не заботясь о скорости. Это безумно медленно, даже в корпусе 0, 0.


Балансировка стек FP:

бросает исключение, если переполнение стека или недостаточный

FP исключение замаскированы по умолчанию в большинстве операционных систем. Еще более важной частью поведения является то, что ST0 содержит мусор после FLD, который вызвал переполнение. Итак, ваш вывод прав: следуя правилам ABI для стека x87, важно: стека пустым в вызовах функций и пустым или с возвратом возвращаемого значения float/double. (Я не знаю ни АБИС, которые делают вещи по-другому, но вы могли бы иметь соглашение о вызовах, которые прошли некоторые арг FP в регистрах x87 вместо стека.)


C Calling Convention

Для всех платформ x86 нет единого соглашения о вызове C. Многие из 32-битных передают в стек стек double и возвращают их в ST (0), как и вы. Так что это вроде нормально, за исключением терминологии.

В обычных 64-битных соглашениях на вызов в XMM-регистры передаются аргументы double (каждый аргумент в нижнем элементе собственного регистра). Существуют также 32-битные соглашения о вызове, которые принимают SSE2 и проходят также double. В этом случае:

; 64-bit Windows or non-Windows, or 32-bit-with-double-in-SSE2 calling convention: 
global dmax 
section .text 
dmax: 
    maxsd xmm0, xmm1 
    ret 

Yep, there's an instruction for std::max(double,double). На данный момент вызов функции имеет больше накладных расходов, чем инструкция, и использование функции asm вместо того, чтобы позволить компилятору C встроить C-функцию в эту инструкцию, является ужасной идеей. Особенно в вызовах условных обозначений (например, System V, которые используются не-Windows), где все регистры XMM вызываются вызовом, поэтому вызывающему абоненту приходится сохранять/восстанавливать все временные файлы и float в памяти вызовов функций.


Если вы должны были написать это с инструкциями x87

fcomp st0 не лучший способ просто вытолкнуть стек x87. Для этого используйте fstp st0.

Похоже, вы предполагаете процессор P6 или более новый (поскольку вы используете FCOMI/FCOMIP), поэтому вы можете также использовать FCMOVcc, а не использовать ветки.

; 32-bit args-on-the-stack 
section .text 
; when one input is NaN, might return NaN or might return the other input 
; This implements the C expression (d1 < d2) 
global dmax 
dmax: 
    fld  qword [esp+12] 
    fld  qword [esp+4]  ; ST0 = d1 and ST1 = d2 

    fucomi st0, st1 
    jp  handle_nan   ; optional. MAXSD does this for free. If you leave this out, I suggest using fcomi instead of fucomi, to raise #IA on NaN 
    FCMOVb st0, st1   ; st0 = (st0<st1) : st1 : st0. (Also copies if unordered because CF=1 in that case, too. But we don't know which operand was NaN.) 

    ;; our return value is in st0, but st1 is still in use. 
    fstp st1    ; pop the stack while keeping st0. (store it to st1, which becomes st0 after popping) 
    ; alternative: ffree st1 ; I think this should work 
    ret 

handle_nan: 
    faddp      ; add both args together to get a NaN, whichever one was NaN to start with. 
    ret 

У этого есть одна очень предсказуемая ветвь (NaN, вероятно, никогда не бывает в реальном использовании, иначе это всегда происходит). Критический путь - это кругооборот по памяти для прохождения arg (~ 5 циклов), затем fucomi (?) -> fcmov (2c) -> fstp st1 (1c). Эти значения циклов для Intel Haswell. Общая латентность = вероятно 5 + 5 (при условии 2c для FUCOMI).

Использование FFREE st1 (если это работает), выведет окончательный fstp с критического пути. FXCHG (нулевая латентность), а затем popping st0 также может вывести это из критического пути. Intel могла бы реализовать FSTP ST1 с нулевой задержкой, например FXCHG (обрабатывается на этапе переименования регистров), но я не думаю, что это так на любой существующей микроархитектуре. (И вряд ли это будет будущей функцией, потому что x87 в основном устарел. IIRC, Intel Skylake немного снизили пропускную способность некоторых вещей x87 против Haswell, сделав больше инструкций x87 общим портом исполнения.)

Intel Haswell пропускная способность: таблица Agner Fog не отображает задержку для FUCOMI, но это 3 раза. FCMOV также имеет 3 выхода с задержкой в ​​2 цикла. Реализация ветвления (возможно, условно запускаемая FXCHG перед всплытием st0) может быть хорошей, если она используется в случае, когда она очень хорошо предсказана. В любом случае, общее количество UOP:

  • 2x ДПД: 2 микрооперации для port2 или порту3
  • FUCOMI: 3 микроопераций для р0/p1
  • ОКК: 1 микрооперации для P0/p6 (предполагается, что предсказывал-not- принято)
  • FCMOV: 3 микрооперации (2p0 1p5)
  • FSTP рег: 1 моп для p0/p1
  • RET:. 1 моп для p6 (микро-слитых с нагрузкой для P237 Это смешно, я думал, p7 был только для простых адресов магазина. Возможно, опечатка в таблице)

общее количество разрешенных доменов: 10 (не считая ret). Так что для этого требуется 2,5 цикла (в группах по 4). На конкретном порт выполнения могут быть узкие места, но я не проверял это.


Оказывается НКА согласна с моим выбором реализации :):

see the code on the Godbolt compiler explorer, скомпилированный с gcc6.2 -m32 -mfpmath=387 -O3 -march=haswell

double dmax(double a, double b) { return a<b ? b : a; } 

    fld  QWORD PTR [esp+4] 
    fld  QWORD PTR [esp+12] ;; it doesn't matter which order you load args in, IDK why I chose reverse order 
    fucomi st, st(1) 
    fcmovbe st, st(1)    ;; moving when they're equal matches the C, but of course doesn't matter 
    fstp st(1) 
    ret 
+0

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