Вот модификация превосходного ответа Кассио Ренана.Он заменяет все специфичные для компилятора расширения стандартным C ++ и теоретически переносим на любой соответствующий компилятор.Кроме того, он проверяет, что аргументы правильно выровнены, а не предполагают это.Он оптимизируется под тот же код.
#include <assert.h>
#include <cmath>
#include <stddef.h>
#include <stdint.h>
#define ALIGNMENT alignof(max_align_t)
using std::floor;
// Compiled with: -std=c++17 -Wall -Wextra -Wpedantic -Wconversion -fno-trapping-math -O -march=cannonlake -mprefer-vector-width=512
void testFunction(const float in[], int32_t out[], const ptrdiff_t length)
{
static_assert(sizeof(float) == sizeof(int32_t), "");
assert((uintptr_t)(void*)in % ALIGNMENT == 0);
assert((uintptr_t)(void*)out % ALIGNMENT == 0);
assert((size_t)length % (ALIGNMENT/sizeof(int32_t)) == 0);
alignas(ALIGNMENT) const float* const input = in;
alignas(ALIGNMENT) int32_t* const output = out;
// Do the conversion
for (int i = 0; i < length; ++i) {
output[i] = static_cast<int32_t>(floor(input[i]));
}
}
Это не так хорошо оптимизирует GCC, как оригинал, который использовал непереносимые расширения.Стандарт C ++ поддерживает спецификатор alignas
, ссылки на выровненные массивы и функцию std::align
, которая возвращает выровненный диапазон в буфере.Однако ни один из них не позволяет любому протестированному мной компилятору генерировать выровненные, а не выровненные векторные нагрузки и хранилища.
Хотя alignof(max_align_t)
равно только 16 для x86_64, и можно определить ALIGNMENT
как константу 64Это не помогает компилятору генерировать лучший код, поэтому я остановился на переносимости.Наиболее близким к переносимому способу заставить компилятор предположить, что poitner выровнен, было бы использование типов из <immintrin.h>
, которые поддерживаются большинством компиляторов для x86, или определение struct
со спецификатором alignas
.Проверяя предопределенные макросы, вы также можете расширить макрос до __attribute__ ((aligned (ALIGNMENT)))
в компиляторах Linux или __declspec (align (ALIGNMENT))
в компиляторах Windows, и что-то безопасное в компиляторе, о котором мы не знаем, но GCC нужен атрибут в введите для фактического создания выровненных загрузок и хранилищ.
Кроме того, в оригинальном примере называлось встроенное сообщение, чтобы сообщить GCC, что для length
невозможно не быть кратным 32. Если вы assert()
это или вызов стандартной функции, такой как abort()
, ни GCC, ни Clang, ни ICC не сделают одно и то же вычитание.Поэтому большая часть кода, который они генерируют, будет обрабатывать случай, когда length
не является хорошим кратным кратным ширины вектора.
Вероятная причина этого заключается в том, что ни одна оптимизация не даст вам такой большой скорости: невыровненная памятьинструкции с выровненными адресами выполняются на процессорах Intel быстро, а код для обработки случая, когда length
не является подходящим круглым числом, имеет длину несколько байтов и выполняется в постоянное время.
В качестве сноски GCCвозможность оптимизировать встроенные функции из <cmath>
лучше, чем макросы, реализованные в <math.c>
.
GCC 9.1 требуется особый набор параметров для генерации кода AVX512.По умолчанию, даже с -march=cannonlake
, он предпочтет 256-битные векторы.Для генерации 512-битного кода требуется -mprefer-vector-width=512
.(Спасибо Peter Cordes за указание на это.) Далее следует векторизованный цикл с развернутым кодом для преобразования любых оставшихся элементов массива.
Вот векторизованный основной цикл, за исключением некоторой инициализации с постоянным временем, ошибка-проверяющий и очищающий код, который будет запускаться только один раз:
.L7:
vrndscaleps zmm0, ZMMWORD PTR [rdi+rax], 1
vcvttps2dq zmm0, zmm0
vmovdqu32 ZMMWORD PTR [rsi+rax], zmm0
add rax, 64
cmp rax, rcx
jne .L7
Орлиный глаз заметит два отличия от кода, сгенерированного программой Кассио Ренана: он использует% zmm вместо% ymm регистров исохраняет результаты с невыровненным vmovdqu32
, а не с выровненным vmovdqa64
.
Clang 8.0.0 с одинаковыми флагами делает различные варианты развертывания циклов.Каждая итерация работает с восемью 512-битными векторами (то есть 128 плавающими одинарной точности), но код для сбора остатков не разворачивается.Если после этого осталось не менее 64 операций с плавающей запятой, он использует для них еще четыре инструкции AVX512, а затем очищает все дополнения с помощью невекторизованного цикла.
Если вы скомпилируете исходную программу в Clang ++, она приметбез претензий, но не будет выполнять те же оптимизации: он по-прежнему не будет предполагать, что length
кратно векторной ширине или что указатели выровнены.
Предпочитает код AVX512 AVX256.даже без -mprefer-vector-width=512
.
test rdx, rdx
jle .LBB0_14
cmp rdx, 63
ja .LBB0_6
xor eax, eax
jmp .LBB0_13
.LBB0_6:
mov rax, rdx
and rax, -64
lea r9, [rax - 64]
mov r10, r9
shr r10, 6
add r10, 1
mov r8d, r10d
and r8d, 1
test r9, r9
je .LBB0_7
mov ecx, 1
sub rcx, r10
lea r9, [r8 + rcx]
add r9, -1
xor ecx, ecx
.LBB0_9: # =>This Inner Loop Header: Depth=1
vrndscaleps zmm0, zmmword ptr [rdi + 4*rcx], 9
vrndscaleps zmm1, zmmword ptr [rdi + 4*rcx + 64], 9
vrndscaleps zmm2, zmmword ptr [rdi + 4*rcx + 128], 9
vrndscaleps zmm3, zmmword ptr [rdi + 4*rcx + 192], 9
vcvttps2dq zmm0, zmm0
vcvttps2dq zmm1, zmm1
vcvttps2dq zmm2, zmm2
vmovups zmmword ptr [rsi + 4*rcx], zmm0
vmovups zmmword ptr [rsi + 4*rcx + 64], zmm1
vmovups zmmword ptr [rsi + 4*rcx + 128], zmm2
vcvttps2dq zmm0, zmm3
vmovups zmmword ptr [rsi + 4*rcx + 192], zmm0
vrndscaleps zmm0, zmmword ptr [rdi + 4*rcx + 256], 9
vrndscaleps zmm1, zmmword ptr [rdi + 4*rcx + 320], 9
vrndscaleps zmm2, zmmword ptr [rdi + 4*rcx + 384], 9
vrndscaleps zmm3, zmmword ptr [rdi + 4*rcx + 448], 9
vcvttps2dq zmm0, zmm0
vcvttps2dq zmm1, zmm1
vcvttps2dq zmm2, zmm2
vcvttps2dq zmm3, zmm3
vmovups zmmword ptr [rsi + 4*rcx + 256], zmm0
vmovups zmmword ptr [rsi + 4*rcx + 320], zmm1
vmovups zmmword ptr [rsi + 4*rcx + 384], zmm2
vmovups zmmword ptr [rsi + 4*rcx + 448], zmm3
sub rcx, -128
add r9, 2
jne .LBB0_9
test r8, r8
je .LBB0_12
.LBB0_11:
vrndscaleps zmm0, zmmword ptr [rdi + 4*rcx], 9
vrndscaleps zmm1, zmmword ptr [rdi + 4*rcx + 64], 9
vrndscaleps zmm2, zmmword ptr [rdi + 4*rcx + 128], 9
vrndscaleps zmm3, zmmword ptr [rdi + 4*rcx + 192], 9
vcvttps2dq zmm0, zmm0
vcvttps2dq zmm1, zmm1
vcvttps2dq zmm2, zmm2
vcvttps2dq zmm3, zmm3
vmovups zmmword ptr [rsi + 4*rcx], zmm0
vmovups zmmword ptr [rsi + 4*rcx + 64], zmm1
vmovups zmmword ptr [rsi + 4*rcx + 128], zmm2
vmovups zmmword ptr [rsi + 4*rcx + 192], zmm3
.LBB0_12:
cmp rax, rdx
je .LBB0_14
.LBB0_13: # =>This Inner Loop Header: Depth=1
vmovss xmm0, dword ptr [rdi + 4*rax] # xmm0 = mem[0],zero,zero,zero
vroundss xmm0, xmm0, xmm0, 9
vcvttss2si ecx, xmm0
mov dword ptr [rsi + 4*rax], ecx
add rax, 1
cmp rdx, rax
jne .LBB0_13
.LBB0_14:
pop rax
vzeroupper
ret
.LBB0_7:
xor ecx, ecx
test r8, r8
jne .LBB0_11
jmp .LBB0_12
ICC 19 также генерирует инструкции AVX512, но сильно отличается от clang
.Он больше настраивается с помощью магических констант, но не разворачивает циклы, работая вместо этого на 512-битных векторах.
Этот кодтак работает на других компиляторах и архитектурах. (Хотя MSVC поддерживает только ISA до AVX2 и не может автоматически векторизовать цикл.) Например, в ARM с -march=armv8-a+simd
он создает векторизованный цикл с frintm v0.4s, v0.4s
и fcvtzs v0.4s, v0.4s
.
Попробуй сам .