web-dev-qa-db-fra.com

Fonction de plancher entier efficace en C ++

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.

32
Yves Daoust

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.

Essayez-le pour vous-même .

1
Davislor