Соответствует ли ICC спецификациям C99 для умножения комплексных чисел?

Рассмотрим этот простой код:

#include <complex.h>
complex float f(complex float x) {
  return x*x;
}

Если вы скомпилируете его с помощью -O3 -march=core-avx2 -fp-model strict с помощью компилятора Intel, вы получите:

f:
        vmovsldup xmm1, xmm0                                    #3.12
        vmovshdup xmm2, xmm0                                    #3.12
        vshufps   xmm3, xmm0, xmm0, 177                         #3.12
        vmulps    xmm4, xmm1, xmm0                              #3.12
        vmulps    xmm5, xmm2, xmm3                              #3.12
        vaddsubps xmm0, xmm4, xmm5                              #3.12
        ret 

Это гораздо более простой код, чем вы получаете от gcc и clang, а также гораздо проще, чем код, который вы найдете в Интернете для умножения сложных чисел. Это не означает, например, явное обращение с комплексными NaN или бесконечностями.

Соответствует ли эта сборка спецификациям для комплексного умножения C99?

Ответы

Ответ 1

Код не соответствует.

Приложение G, раздел 5.1, пункт 4, читает

Операторы * и / удовлетворяют следующим бесконечным свойствам для всех действительных, мнимых и комплексных операндов:

- если один операнд бесконечен, а другой операнд - ненулевое конечное число или бесконечность, то результат оператора * является бесконечностью;

Итак, если z= a * ib бесконечно и w= c * id бесконечно, число z * w должен быть бесконечным.

В том же приложении, раздел 3, параграф 1, определяется, что означает, что для комплексного числа будет бесконечным:

Комплексное или мнимое значение с по крайней мере одной бесконечной частью рассматривается как бесконечность (даже если ее другая часть является NaN).

So z бесконечно, если либо a, либо b. Это действительно разумный выбор, поскольку он отражает математическую структуру 1.

Однако, если мы допустим z= ∞ + i∞ (бесконечное значение) и w= i∞ (и бесконечное значение), результат для кода Intel z * w= NaN + iNaN из-за промежуточных продуктов ∞ · 0 2.

Это достаточно, чтобы обозначить его как несоответствующее.


Мы можем еще раз подтвердить это, взглянув на сноску первой цитаты (сноска здесь не сообщается), она упоминает директиву CX_LIMITED_RANGE pragma.

Раздел 7.3.4, пункт 1 гласит

Обычные математические формулы для комплексного умножения, деления и абсолютного значения являются проблематичными из-за их обработки бесконечностей и из-за чрезмерного переполнения и недостаточного потока. Прагма CX_LIMITED_RANGE может использоваться для информирования реализации о том, что (там, где состояние "включено" ) приемлемы обычные математические формулы [, которые производят NaN].

Здесь стандартный комитет пытается смягчить огромный моль работы для комплексного умножения (и деления).
Фактически GCC имеет флаг для контроля этого поведения:

-fcx-limited-range
Когда этот параметр включен, этот параметр указывает, что при выполнении сложного деления не требуется этап сокращения диапазона.

Также не проверяется, является ли результат комплексного умножения или деления NaN + я * NaN с попыткой спасти ситуацию в этом случае.

Значение по умолчанию -fno-cx-limited-range, но включено -ffast-math.
Эта опция управляет установкой по умолчанию для ISO C99 CX_LIMITED_RANGE прагмы.

Только этот параметр заставляет GCC генерировать медленный код и дополнительные проверки, без него код, который он генерирует, имеет те же недостатки Intel один (я перевел источник на C++)

f(std::complex<float>):
        movq    QWORD PTR [rsp-8], xmm0
        movss   xmm0, DWORD PTR [rsp-8]
        movss   xmm2, DWORD PTR [rsp-4]
        movaps  xmm1, xmm0
        movaps  xmm3, xmm2
        mulss   xmm1, xmm0
        mulss   xmm3, xmm2
        mulss   xmm0, xmm2
        subss   xmm1, xmm3
        addss   xmm0, xmm0
        movss   DWORD PTR [rsp-16], xmm1
        movss   DWORD PTR [rsp-12], xmm0
        movq    xmm0, QWORD PTR [rsp-16]
        ret

Без этого код

f(std::complex<float>):
        sub     rsp, 40
        movq    QWORD PTR [rsp+24], xmm0
        movss   xmm3, DWORD PTR [rsp+28]
        movss   xmm2, DWORD PTR [rsp+24]
        movaps  xmm1, xmm3
        movaps  xmm0, xmm2
        call    __mulsc3
        movq    QWORD PTR [rsp+16], xmm0
        movss   xmm0, DWORD PTR [rsp+16]
        movss   DWORD PTR [rsp+8], xmm0
        movss   xmm0, DWORD PTR [rsp+20]
        movss   DWORD PTR [rsp+12], xmm0
        movq    xmm0, QWORD PTR [rsp+8]
        add     rsp, 40
        ret

а функция __mulsc3 практически та же, что и стандарт C99 для комплексного умножения.
Он включает вышеупомянутые проверки.


1 Там, где модуль числа продолжается от реального случая | z | на комплексный ‖z‖, сохраняя определение бесконечности в результате неограниченных пределов. Проще говоря, в комплексной плоскости существует целая окружность бесконечных значений, и для получения бесконечного модуля требуется только одна "координата" .

2 Ситуация становится худшей, если мы помним, что z= NaN + i∞ или z= ∞ + iNaN являются допустимыми бесконечными значениями

Ответ 2

Я получаю аналогичный, но не идентичный код от clang 3.8 at -O2 -march=core-avx2 -ffast-math: я не очень хорошо знаком с функциями x86 с плавающей запятой последнего дня, но я думаю, что он делает одни и те же вычисления, но использует разные инструкции для перетасовки значений вокруг в регистрах.

f:
        vmovshdup       %xmm0, %xmm1    # xmm1 = xmm0[1,1,3,3]
        vaddss  %xmm0, %xmm0, %xmm2
        vmulss  %xmm2, %xmm1, %xmm2
        vmulss  %xmm1, %xmm1, %xmm1
        vfmsub231ss     %xmm0, %xmm0, %xmm1
        vinsertps       $16, %xmm2, %xmm1, %xmm0 # xmm0 = xmm1[0],xmm2[0],xmm1[2,3]
        retq

GCC 6.3, с теми же параметрами, снова, похоже, выполняет одни и те же вычисления, но перемешивает значения еще третьим способом:

f:
        vmovq   %xmm0, -8(%rsp)
        vmovss  -4(%rsp), %xmm2
        vmovss  -8(%rsp), %xmm0
        vmulss  %xmm2, %xmm2, %xmm1
        vfmsub231ss     %xmm0, %xmm0, %xmm1
        vmulss  %xmm2, %xmm0, %xmm0
        vmovss  %xmm1, -16(%rsp)
        vaddss  %xmm0, %xmm0, %xmm0
        vmovss  %xmm0, -12(%rsp)
        vmovq   -16(%rsp), %xmm0
        ret

Без -ffast-math оба компилятора генерируют существенно другой код, который, по-видимому, проверяет для NaN, по крайней мере.

Я заключу, что компилятор Intel не генерирует комплексное умножение, совместимое с IEEE, даже с -fp-model strict. Могут быть и другие ключи командной строки, которые позволяют полностью генерировать IEEE-совместимый код.

Независимо от того, квалифицируется ли это как нарушение C99, зависит от того, документирован ли компилятор Intel в соответствии с Приложением F и G (в котором указывается, что это означает для реализации C, чтобы обеспечить реальную и сложную арифметику, совместимую с IEEE) и если это так, какие параметры командной строки вы должны дать, чтобы получить соответствующий режим.