Je veux définir une fonction de plancher entier efficace, c'est-à-dire une conversion de flotteur ou double qui effectue la troncature vers Minus Infinity.
Nous pouvons supposer que les valeurs sont telles qu'aucun débordement entier ne se produit. Jusqu'à présent j'ai quelques options
casting to int; Cela nécessite une manipulation spéciale des valeurs négatives, car la fonte tronque vers zéro;
I= int(F); if (I < 0 && I != F) I--;
jeter le résultat du sol à int;
int(floor(F));
jeter sur Int avec un changement important pour obtenir des points positifs (cela peut renvoyer de mauvais résultats pour les grandes valeurs);
int(F + double(0x7fffffff)) - 0x7fffffff;
La coulée to int est notoirement lente. Donc sont si des tests. Je n'ai pas chronométré la fonction de plancher, mais des postes affirmés prétendent que c'est aussi lent.
Pouvez-vous penser à de meilleures alternatives en termes de vitesse, de précision ou de portée autorisée? Il n'a pas besoin d'être portable. Les objectifs sont des architectures récentes X86/X64.
Voici une modification de la réponse excellente de Cássio Renan. Il remplace toutes les extensions spécifiques au compilateur avec standard C++ et est en théorie, portable à tout compilateur conforme. En outre, il vérifie que les arguments sont correctement alignés plutôt que de la supposer. Il optimise le même code.
#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]));
}
}
Cela n'opit pas aussi bien que le GCC que l'original, qui a utilisé des extensions non portables. La norme C++ prend en charge un spécificateur alignas
, des références à des matrices alignées et une fonction std::align
qui renvoie une plage alignée dans un tampon. Aucun de ceux-ci, cependant, tout compilateur que j'ai testé généré aligné au lieu de charges et de magasins vectoriels non alignés.
Bien que alignof(max_align_t)
ne soit que 16 sur x86_64, et il est possible de définir ALIGNMENT
comme la constante 64, cela n'aide pas le compilateur générer un meilleur code, donc je suis allé pour la portabilité. La chose la plus proche à une manière portable de forcer le compilateur à supposer qu'un Poiner est aligné serait d'utiliser les types de <immintrin.h>
, que la plupart des compilateurs de support X86 ou définissent un struct
avec un alignas
SPÉCIFICATEUR. En vérifiant les macros prédéfinies, vous pouvez également développer une macro à __attribute__ ((aligned (ALIGNMENT)))
sur compilateurs Linux ou __declspec (align (ALIGNMENT))
sur les compilateurs Windows, et quelque chose de sécurité sur un compilateur que nous ne connaissons pas, mais GCC Besoin de l'attribut sur un type pour générer des charges et des magasins alignés.
De plus, l'exemple original appelé un bulit-in pour dire à GCC qu'il était impossible pour length
ne pas être un multiple de 32. Si vous assert()
Ceci ou appelez une fonction standard telle que abort()
, ni GCC, Clang ni ICC ne feront la même déduction. Par conséquent, la plupart du code qu'ils génèrent géreront le cas où length
n'est pas un bon multiple rond de la largeur de vecteur.
Une raison probable à ce titre est que l'on n'oblige aucune optimisation qui ne vous obtiendra aucune vitesse: les instructions de mémoire non alignées avec des adresses alignées sont rapides sur les processeurs Intel et le code pour gérer le cas où length
n'est pas un bon nombre de ronds. octets longs et fonctionne en temps constant.
En tant que note de bas de page, GCC est capable d'optimiser les fonctions en ligne de <cmath>
mieux que les macros implémentées dans <math.c>
.
GCC 9.1 nécessite un ensemble particulier d'options pour générer du code AVX512. Par défaut, même avec -march=cannonlake
, il préférera des vecteurs de 256 bits. Il a besoin du -mprefer-vector-width=512
pour générer du code 512 bits. (Merci à Peter Cordes pour le pointer de cette sortie.) Il est suivant la boucle vectorisée avec un code déroulé pour convertir les éléments de restes de la matrice.
Voici la boucle principale vectorisée, moins une initialisation de temps constant, une vérification des erreurs et un code de nettoyage qui ne sera exécutée qu'une seule fois:
.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
Les yeux d'aigle remarqueront deux différences du code généré par le programme de Cássio Renan: il utilise% ZMM au lieu de% de registres YMM, et il stocke les résultats avec un vmovdqu32
non aligné plutôt que d'un vmovdqa64
aligné.
Clang 8.0.0 avec les mêmes drapeaux fait des choix différents sur les boucles déroulantes. Chaque itération fonctionne sur huit vecteurs de 512 bits (c'est-à-dire des flotteurs de 128 précision), mais le code à ramasser des restes n'est pas déroulé. S'il y a au moins 64 floats laissés après cela, il utilise quatre autres instructions AVX512 pour celles-ci, puis nettoie tous les extras avec une boucle non dévidée.
Si vous compilez le programme d'origine à Clang ++, cela l'acceptera sans plainte, mais ne prendra pas les mêmes optimisations: il ne supposera toujours pas que le length
est un multiple de la largeur de vecteur, ni que les pointeurs sont alignés.
Il préfère le code AVX512 à AVX256, même sans -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 génère également des instructions AVX512, mais très différente de clang
. Il fait plus de configuration avec des constantes magiques, mais ne déroule aucune boucle, opérant à la place des vecteurs de 512 bits.
Ce code travaille également sur d'autres compilateurs et architectures. (Bien que MSVC prend en charge uniquement le ISA jusqu'à AVX2 et ne peut pas vectoriser automatiquement la boucle.) On ARM avec -march=armv8-a+simd
, par exemple, il génère une boucle vectorielle avec frintm v0.4s, v0.4s
et fcvtzs v0.4s, v0.4s
.