Тип punning с (float &) int works, (float const &) int вместо этого конвертирует как (float) int? - PullRequest
0 голосов
/ 31 января 2019

VS2019, Release, x86.

template <int i> float get() const {
    int f = _mm_extract_ps(fmm, i);
    return (float const&)f;
}

При использовании return (float&)f; используется компилятор

extractps m32, ...
movss xmm0, m32

. Правильный результат

При использовании return (float const&)f; компилятор использует

extractps eax, ...
movd xmm0, eax

. Неправильный результат

Основная идея, что T & и T const & сначала T, а затем const.Const - это просто соглашение для программистов.Вы знаете, что вы можете обойти это.Но в ассемблерном коде НЕТ никакого const, кроме типа float IS.И я думаю, что как для float &, так и для float const & это ДОЛЖНО быть представление с плавающей точкой (регистр процессора) в сборке.Мы можем использовать промежуточный тип int reg32, но окончательная интерпретация должна быть плавающей.

И в настоящее время это выглядит как регрессия, потому что раньше это работало нормально.А также использование float & в этом случае определенно странно, потому что мы не должны рассматривать const const и безопасность, но temp var для float & действительно сомнительный.

Microsoft ответила:

Hi TruthfinderСпасибо за отдельное воспроизведение.Как это бывает, это поведение на самом деле правильно.Как описал мой коллега @Xiang Fan [MSFT] во внутренней электронной почте:

Преобразования, выполняемые [приведением в стиле c], пытаются выполнить следующую последовательность: (4.1) - const_cast (7.6.1.11), (4.2) - static_cast (7.6.1.9), (4.3) - static_cast с последующим const_cast, (4.4) - reinterpret_cast (7.6.1.10) или (4.5) - reinterpret_cast, за которым следует const_cast,

Если преобразование может быть интерпретировано более чем одним из перечисленных выше способов, используется интерпретация, которая появляется первой в списке.

Таким образом, в вашем случае (const float &) преобразуется в static_cast,что приводит к тому, что «выражение инициализатора неявно преобразуется в значение типа« cv1 T1 ». Применяется преобразование временной материализации, и ссылка привязывается к результату».

Но в другом случае (float &) преобразуется в reinterpret_cast, потому что static_cast недействителен, что совпадает с reinterpret_cast (& operand).

Фактическая «ошибка», которую вы наблюдаетеВинг заключается в том, что одно приведение выполняет: «преобразовывает значение с плавающей запятой« 1.0 »в эквивалентное значение с типом int« 1 »», в то время как другое приведение говорит «найдите представление бита 1.0 как число с плавающей запятой, а затем интерпретируйте эти биты».как int ".

По этой причине мы рекомендуем не использовать C-style.

Спасибо!

Ссылка на форум MS: https://developercommunity.visualstudio.com/content/problem/411552/extract-ps-intrinsics-bug.html

Есть идеи?

PS Что я действительно хочу:

float val = _mm_extract_ps(xmm, 3);

При ручной сборке я могу написать: extractps val, xmm0, 3 где val - переменная памяти типа float 32.Только один!инструкция.Я хочу увидеть тот же результат в сгенерированном компилятором коде сборки.Никаких перетасовок или каких-либо других излишних инструкций.Самый плохой приемлемый случай: extractps reg32, xmm0, 3; mov val, reg32.

Моя точка зрения о T & и T const &: Тип переменной должен быть ОДНОМ для обоих случаев.Но теперь float& будет интерпретировать m32 как float32, а float const& будет интерпретировать m32 как int32.

int main() {
    int z = 1;
    float x = (float&)z;
    float y = (float const&)z;
    printf("%f %f %i", x, y, x==y);
    return 0;
}

Out: 0.000000 1.000000 0

Это действительно нормально??

С наилучшими пожеланиями, Truthfinder

Ответы [ 4 ]

0 голосов
/ 04 февраля 2019

Я вижу, что кто-то любит устанавливать минусы.Похоже, я был почти прав насчет *(float*)&.Но лучший способ, конечно, это использовать стандартное intrin .h решение для кросс-компиляции.MSVS, smmintrin.h:

#define _MM_EXTRACT_FLOAT(dest, src, ndx) \
        *((int*)&(dest)) = _mm_extract_ps((src), (ndx))

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

0 голосов
/ 31 января 2019

Моя точка зрения по поводу T& и T const&: тип переменной должен быть ОДИНАКОВЫМ для обоих случаев.

Как пытается объяснить поддержка MicrosoftНет, это не то же самое.Так работает C ++.

Вы используете приведение в стиле C ( ... ), которое в C ++ разбивается на серию попыток использовать различные преобразования C ++ в порядке убывания безопасности:

  • (4.1) - a const_cast
  • (4.2) - a static_cast
  • (4.3) - a static_cast, за которым следует const_cast
  • (4.4) - a reinterpret_cast
  • (4.5) - reinterpret_cast, за которым следует const_cast

В случае (float const&) b (где b - int):

  • Мы стараемся const_cast<float const&>(b); - не повезло (float против int)
  • Мы пытаемся static_cast<float const&>(b); - вуаля!(после неявного стандартного преобразования b во временное float - помните, что C ++ позволяет выполнять два стандартных и одно пользовательское преобразование для выражения неявно )

В случае (float&) b (опять же, где b является int):

  • Мы пытаемся const_cast<float&>(b); - не повезло
  • Мы пытаемся static_cast<float&>(b); - не повезло (после неявного стандартного преобразования b во временное float, оно не будет привязано к не const lvalue ссылке)
  • Мы пытаемся const_cast<float&>(static_cast<float&>(b)); - не повезло
  • Мы попробуем reinterpret_cast<float&>(b); - вуаля!

Строгое правило псевдонимов в стороне 1 , вот пример, демонстрирующий это поведение:

#include <iostream>

int main() {
    float a = 1.2345f;
    int b = reinterpret_cast<int&>(a); // this type-pun is built into _mm_extract_ps
    float nc = (float&)b;
    float cc = (float const&)b;
    float rc = reinterpret_cast<float&>(b);
    float sc = static_cast<float const&>(b);
    std::cout << "a=" << a << " b=" << b << std::endl;
    std::cout << "nc=" << nc << " cc=" << cc << std::endl;
    std::cout << "rc=" << rc << " sc=" << sc << std::endl;
}

Отпечатки:

a=1.2345 b=1067320345
nc=1.2345 cc=1.06732e+09
rc=1.2345 sc=1.06732e+09

LIVE DEMO

Вот почему вы не должны использовать приведения в стиле C в C ++.Меньше печатать, но намного больше головной боли.

Также не используйте _mm_extract_ps - причина, по которой он возвращает int, заключается в том, что инструкция extractps копирует floatв общий регистр - это , а не , что вы хотите, поскольку для использования a float его необходимо скопировать обратно в регистр с плавающей запятой.Так что это пустая трата времени.Как объясняет Питер Кордес, вместо этого используйте _mm_cvtss_f32(_mm_shuffle_ps()), который компилируется в одну инструкцию.


1 Технически говоря, используя reinterpret_cast, чтобы обойти систему типов C ++ (также известный как typeнаказание) - неопределенное поведение в ISO C ++.Однако MSVC ослабляет это правило как расширение компилятора.Таким образом, код верен, если он скомпилирован с MSVC или где-то еще, где может быть отключено правило строгого алиасинга * (например, -fno-strict-aliasing).Стандартный способ печатать слова, не попадая в ловушку строгого алиасинга, - memcpy().

0 голосов
/ 31 января 2019

Хорошо.Похоже, идея, когда float val = _mm_extract_ps(xmm, 3) может быть скомпилирована в одну инструкцию extractps val, xmm0, 3, недоступна.

И я все еще использую *(float*)&intval, потому что она будет работать предсказуемо на любой версии msvc.

Что касается int _mm_extract_ps, это определенно плохой дизайн._ps используется тип float, а epi32 используется для типа int32.Инструкция extractps не напечатана, поэтому это должны быть две разные функции int _mm_extract_epi32(__m128i(), 3) и float _mm_extract_ps(__m128(), 3).

PS http://aras -p.info / blog / 2018/12/28 / Modern-C-Lamentations /

Я не знаю, почему это решение было принято языковым комитетом или кем-либо еще, но memcpy просто not beautiful.И также я уверен, что это создает дополнительные проблемы для компилятора, и нет никакого способа для единственного результата инструкции.Как я понимаю, рекомендуемое решение - int i = _mm_extract_ps(...); float f; std::memcpy(&f, &i, sizeof(f));.Что касается меня, то float f = static_cast<float const&>(_mm_extract_ps(...)); проще, проще и понятнее.Ref потому что функция возвращает значение, а не указатель, const, потому что вы не можете его изменить.Это выглядит как интуитивное решение.Const это только проблема компилятора, в окончательной сборке нет инструкции const.

0 голосов
/ 31 января 2019

Есть интересный вопрос о семантике приведения C ++ (на который Microsoft уже кратко ответил вам), но он смешивается с неправильным использованием _mm_extract_ps, что приводит к необходимости в первую очередь каламбура. (И только показывая asm, который эквивалентен, опуская преобразование int-> float.) Если кто-то хочет расширить стандартное в другом ответе, это было бы здорово.

TL: DR: используйте этовместо этого: это ноль или один shufps.Никаких извлечений, никаких типов ошибок.

template <int i> float get(__m128 input) {
    __m128 tmp = input;
    if (i)     // constexpr i means this branch is compile-time-only
        tmp = _mm_shuffle_ps(tmp,tmp,i);  // shuffle it to the bottom.
    return _mm_cvtss_f32(tmp);
}

Если у вас действительно есть сценарий использования памяти, вы должны искать в asm функцию, которая принимает float* выходной аргумент, а не функцию, которая нуждается врезультат в xmm0.(И да, это вариант использования для инструкции extractps, но, возможно, не для встроенных _mm_extract_ps. Gcc и clang используют extractps при оптимизации *out = get<2>(in), хотя MSVC пропускает это и по-прежнему использует shufps + movss.)


Оба блока asm, которые вы показываете, просто копируют куда-то младшие 32 бита xmm0, без преобразования в int.Вы пропустили важные различия и показали только ту часть, которая просто бесполезно копирует битовый шаблон float из xmm0, а затем обратно, двумя различными способами (для регистрации или в память).movd - это чистая копия битов без изменений, точно так же, как и загрузка movss.

Выбор компилятора после того, как вы заставите его вообще использовать extractps.Проход через регистр и обратно имеет меньшую задержку, чем сохранение / перезагрузка, но больше ALU выполняет.

Попытка (float const&) type-pun действительно включает преобразование из FP в целое число, которое вы не делали.т шоу .Как если бы нам нужна была еще одна причина, чтобы избежать приведения указателя / ссылки для определения типа, это действительно означает что-то другое: (float const &) f принимает целочисленный битовый шаблон (из _mm_extract_ps) какint и преобразует это в float.

Я поместил ваш код в проводник компилятора Godbolt , чтобы увидеть, что вы пропустили.

float get1_with_extractps_const(__m128 fmm) {
    int f = _mm_extract_ps(fmm, 1);
    return (float const&)f;
}

;; from MSVC -O2 -Gv  (vectorcall passes __m128 in xmm0)
float get1_with_extractps_const(__m128) PROC   ; get1_with_extractps_const, COMDAT
    extractps eax, xmm0, 1   ; copy the bit-pattern to eax

    movd    xmm0, eax      ; these 2 insns are an alternative to pxor xmm0,xmm0 + cvtsi2ss xmm0,eax to avoid false deps and zero the upper elements
    cvtdq2ps xmm0, xmm0    ; packed conversion is 1 uop
    ret     0

GCC компилирует это следующим образом:

get1_with_extractps_const(float __vector(4)):    # gcc8.2 -O3 -msse4
        extractps       eax, xmm0, 1
        pxor    xmm0, xmm0            ; cvtsi2ss has an output dependency so gcc always does this
        cvtsi2ss        xmm0, eax     ; MSVC's way is probably better for float.
        ret

Очевидно, MSVC определяет поведение приведения указателя / ссылки для определения типа.Обычный ISO C ++ не (строгий псевдоним UB), как и другие компиляторы.Используйте memcpy для ввода слов или объединения (которое GNU C и MSVC поддерживают в C ++ в качестве расширения).Конечно, в этом случае типирование элемента вектора, который вы хотите получить целым числом и обратно, ужасно.

Только для (float &)f gcc предупреждает о нарушении строгого псевдонима. И GCC / clang согласны с MSVC, что только эта версия является каламбуром, не материализуя float из неявного преобразования. C ++ странно!

float get1_with_extractps_nonconst(__m128 fmm) {
    int f = _mm_extract_ps(fmm, 1);
    return (float &)f;
}

<source>: In function 'float get_with_extractps_nonconst(__m128)':
<source>:21:21: warning: dereferencing type-punned pointer will break strict-aliasing rules [-Wstrict-aliasing]
     return (float &)f;
                     ^

gcc оптимизирует прочьextractps в целом.

# gcc8.2 -O3 -msse4
get1_with_extractps_nonconst(float __vector(4)):
    shufps  xmm0, xmm0, 85    ; 0x55 = broadcast element 1 to all elements
    ret

Clang использует SSE3 movshdup для копирования элемента 1 в 0. (И элемента 3 в 2).Но MSVC этого не делает, что является еще одной причиной никогда не использовать это:

float get1_with_extractps_nonconst(__m128) PROC
    extractps DWORD PTR f$[rsp], xmm0, 1     ; store
    movss   xmm0, DWORD PTR f$[rsp]          ; reload
    ret     0

Не используйте _mm_extract_ps для этого

Обе ваши версииужасно, потому что это не то, что _mm_extract_ps или extractps для . Intel SSE: почему `_mm_extract_ps` возвращает` int` вместо `float`?

A float в регистре - это то же самое, что и младший элемент вектора.Высокие элементы не должны быть обнулены.И если бы они это сделали, вы бы хотели использовать insertps, который может делать элементы xmm, xmm и ноль в соответствии с немедленным.

Используйте _mm_shuffle_ps, чтобы перевести нужный элемент в нижнее положение элемента.зарегистрируйтесь, и тогда это будет скалярным числом с плавающей точкой.(И вы можете сказать компилятору C ++, что с _mm_cvtss_f32).Это должно компилироваться в shufps xmm0,xmm0,2, без и extractps или любым mov.

template <int i> float get() const {
    __m128 tmp = fmm;
    if (i)                               // i=0 means the element is already in place
        tmp = _mm_shuffle_ps(tmp,tmp,i);  // else shuffle it to the bottom.
    return _mm_cvtss_f32(tmp);
}

(я пропустил _MM_SHUFFLE(0,0,0,i), потому что это равно i.)

Если бы ваша fmm была в памяти, а не в регистре, то, надеюсь, компиляторы оптимизировали бы случайное перемешивание и просто movss xmm0, [mem].MSVC 19.14 удается это сделать, по крайней мере для аргумента function в случае стека.Я не тестировал другие компиляторы, но clang, вероятно, удастся оптимизировать _mm_shuffle_ps;очень хорошо видеть сквозь тасования.

Тест-кейс, доказывающий, что это эффективно компилируется

Например, тест-кейс с версией вашей функции, не входящей в класс, и вызывающий, который вставляет еедля конкретного i:

#include <immintrin.h>

template <int i> float get(__m128 input) {
    __m128 tmp = input;
    if (i)                  // i=0 means the element is already in place
        tmp = _mm_shuffle_ps(tmp,tmp,i);  // else shuffle it to the bottom.
    return _mm_cvtss_f32(tmp);
}

// MSVC -Gv (vectorcall) passes arg in xmm0
// With plain dumb x64 fastcall, arg is on the stack, and it *does* just MOVSS load without shuffling
float get2(__m128 in) {
    return get<2>(in);
}

Из проводника компилятора Godbolt , вывод asm из MSVC, clang и gcc:

;; MSVC -O2 -Gv
float get<2>(__m128) PROC               ; get<2>, COMDAT
        shufps  xmm0, xmm0, 2
        ret     0
float get<2>(__m128) ENDP               ; get<2>

;; MSVC -O2  (without Gv, so the vector comes from memory)
input$ = 8
float get<2>(__m128) PROC               ; get<2>, COMDAT
        movss   xmm0, DWORD PTR [rcx+8]
        ret     0
float get<2>(__m128) ENDP               ; get<2>
# gcc8.2 -O3 for x86-64 System V (arg in xmm0)
get2(float __vector(4)):
        shufps  xmm0, xmm0, 2   # with -msse4, we get unpckhps
        ret
# clang7.0 -O3 for x86-64 System V (arg in xmm0)
get2(float __vector(4)):
        unpckhpd        xmm0, xmm0      # xmm0 = xmm0[1,1]
        ret

Оптимизатор перемешивания clang упрощается до unpckhpd, что быстрее на некоторых старых процессорах.К сожалению, он не заметил, что мог бы использовать movhlps xmm0,xmm0, который также быстр и на 1 байт короче.

...