web-dev-qa-db-fra.com

Le moyen le plus rapide de bloquer une valeur réelle (virgule fixe/flottante)?

Existe-t-il un moyen plus efficace de verrouiller des nombres réels que d’utiliser des instructions if ou des opérateurs ternaires? Je pas demande un code capable de gérer les deux cas; ils seront traités dans des fonctions séparées.

Évidemment, je peux faire quelque chose comme:

double clampedA;
double a = calculate();
clampedA = a > MY_MAX ? MY_MAX : a;
clampedA = a < MY_MIN ? MY_MIN : a;

ou

double a = calculate();
double clampedA = a;
if(clampedA > MY_MAX)
    clampedA = MY_MAX;
else if(clampedA < MY_MIN)
    clampedA = MY_MIN;

La version du point de fixation utiliserait des fonctions/macros pour les comparaisons.

Ceci est fait dans une partie critique du code, donc je cherche un moyen aussi efficace que possible de le faire (ce qui impliquerait une manipulation de bits). 

EDIT: Il doit s'agir de C standard/portable, la fonctionnalité spécifique à la plate-forme ne présente aucun intérêt ici. De plus, MY_MIN et MY_MAX sont du même type que la valeur que je veux verrouiller (double dans les exemples ci-dessus).

37
Niklas

Pour la représentation 16.16, il est peu probable que le ternaire simple soit amélioré en termes de vitesse.

Et pour les doubles, parce que vous en avez besoin du C standard/portable, le bidouillage de tous les genres finira mal. 

Même si un peu de violon était possible (ce dont je doute), vous vous baseriez sur la représentation binaire des doubles. CECI (et leur taille) IS IMPLEMENTATION-DEPENDENT.

Vous pourriez peut-être "deviner" ceci en utilisant sizeof (double), puis en comparant la disposition de différentes valeurs doubles à leurs représentations binaires communes, mais je pense que vous ne cachez rien.

La meilleure règle est de dire au compilateur ce que vous voulez (c.-à-d. Ternaire) et laissez-le optimiser pour vous.

EDIT: Humble temps de tarte. Je viens de tester l'idée de quinmars (ci-dessous), et cela fonctionne - si vous avez des flotteurs IEEE-754. Cela a donné une accélération d'environ 20% sur le code ci-dessous. Évidemment non-portable, mais je pense qu'il peut exister un moyen standard de demander à votre compilateur s'il utilise les formats flottants IEEE754 avec un #IF ...?

  double FMIN = 3.13;
  double FMAX = 300.44;

  double FVAL[10] = {-100, 0.23, 1.24, 3.00, 3.5, 30.5, 50 ,100.22 ,200.22, 30000};
  uint64  Lfmin = *(uint64 *)&FMIN;
  uint64  Lfmax = *(uint64 *)&FMAX;

    DWORD start = GetTickCount();

    for (int j=0; j<10000000; ++j)
    {
        uint64 * pfvalue = (uint64 *)&FVAL[0];
        for (int i=0; i<10; ++i)
            *pfvalue++ = (*pfvalue < Lfmin) ? Lfmin : (*pfvalue > Lfmax) ? Lfmax : *pfvalue;
    }

    volatile DWORD hacktime = GetTickCount() - start;

    for (int j=0; j<10000000; ++j)
    {
        double * pfvalue = &FVAL[0];
        for (int i=0; i<10; ++i)
            *pfvalue++ = (*pfvalue < FMIN) ? FMIN : (*pfvalue > FMAX) ? FMAX : *pfvalue;
    }

    volatile DWORD normaltime = GetTickCount() - (start + hacktime);
8
Roddy

Vieille question, mais je travaillais sur ce problème aujourd'hui (avec doubles/floats).

La meilleure approche consiste à utiliser SSE MINSS/MAXSS pour les flotteurs et SSE2 MINSD/MAXSD pour les doubles. Celles-ci sont sans branche et prennent un cycle d'horloge chacune, et sont faciles à utiliser grâce aux propriétés intrinsèques du compilateur. Ils confèrent plus d'un gain de performances d'un ordre de grandeur par rapport au serrage avec std :: min/max.

Vous trouverez peut-être cela surprenant. J'ai certainement fait! Malheureusement, VC++ 2010 utilise des comparaisons simples pour std :: min/max même lorsque/Arch: SSE2 et/FP: fast sont activés. Je ne peux pas parler pour d'autres compilateurs.

Voici le code nécessaire pour le faire dans VC++:

#include <mmintrin.h>

float minss ( float a, float b )
{
    // Branchless SSE min.
    _mm_store_ss( &a, _mm_min_ss(_mm_set_ss(a),_mm_set_ss(b)) );
    return a;
}

float maxss ( float a, float b )
{
    // Branchless SSE max.
    _mm_store_ss( &a, _mm_max_ss(_mm_set_ss(a),_mm_set_ss(b)) );
    return a;
}

float clamp ( float val, float minval, float maxval )
{
    // Branchless SSE clamp.
    // return minss( maxss(val,minval), maxval );

    _mm_store_ss( &val, _mm_min_ss( _mm_max_ss(_mm_set_ss(val),_mm_set_ss(minval)), _mm_set_ss(maxval) ) );
    return val;
}

Le code en double précision est le même, à la différence de xxx_sd.

Edit: Au départ, j’ai écrit la fonction clamp comme commentée. Mais en regardant la sortie de l'assembleur, j'ai remarqué que le compilateur VC++ n'était pas assez intelligent pour éliminer le mouvement redondant. Une instruction de moins. :)

37
Spat

GCC et clang génèrent tous deux une superbe Assemblée pour le code simple, simple et portable suivant:

double clamp(double d, double min, double max) {
  const double t = d < min ? min : d;
  return t > max ? max : t;
}

> gcc -O3 -march=native -Wall -Wextra -Wc++-compat -S -fverbose-asm clamp_ternary_operator.c

Assemblée générée par GCC:

maxsd   %xmm0, %xmm1    # d, min
movapd  %xmm2, %xmm0    # max, max
minsd   %xmm1, %xmm0    # min, max
ret

> clang -O3 -march=native -Wall -Wextra -Wc++-compat -S -fverbose-asm clamp_ternary_operator.c

Assemblée générée par Clang:

maxsd   %xmm0, %xmm1
minsd   %xmm1, %xmm2
movaps  %xmm2, %xmm0
ret

Trois instructions (sans compter le ret), pas de branches. Excellent.

Cela a été testé avec GCC 4.7 et 3.2 sur Ubuntu 13.04 avec un Core i3 M 350 . En passant, le simple appel de code C++ std :: min et std :: max ont généré le même assemblage.

C'est pour les doubles. Et pour int, GCC et clang génèrent Assembly avec cinq instructions (sans compter les ret) et aucune branche. Aussi excellent.

Je n'utilise pas actuellement de point fixe, je ne donnerai donc pas d'avis sur un point fixe.

36
Jorge

Si votre processeur a une instruction rapide pour la valeur absolue (comme le fait le x86), vous pouvez effectuer un min et un max sans branche, ce qui est plus rapide qu'une instruction if ou une opération ternaire.

min(a,b) = (a + b - abs(a-b)) / 2
max(a,b) = (a + b + abs(a-b)) / 2

Si l'un des termes est zéro (comme c'est souvent le cas lorsque vous serrez), le code simplifie un peu plus loin:

max(a,0) = (a + abs(a)) / 2

Lorsque vous combinez les deux opérations, vous pouvez remplacer les deux /2 par un seul /4 ou *0.25 pour enregistrer une étape.

Le code suivant est plus de 3 fois plus rapide que ternaire sur mon Athlon II X2, lors de l'utilisation de l'optimisation pour FMIN = 0.

double clamp(double value)
{
    double temp = value + FMAX - abs(value-FMAX);
#if FMIN == 0
    return (temp + abs(temp)) * 0.25;
#else
    return (temp + (2.0*FMIN) + abs(temp-(2.0*FMIN))) * 0.25;
#endif
}
15
Mark Ransom

L’opérateur ternaire est vraiment la voie à suivre, car la plupart des compilateurs sont capables de les compiler dans une opération matérielle native qui utilise un déplacement conditionnel au lieu d’une branche (et évite ainsi les pénalités imprévisibles et les bulles de pipeline, etc.). La manipulation de bits est susceptible de causer un chargement-hit-store .

En particulier, PPC et x86 avec SSE2 ont une opération matérielle qui pourrait être exprimée en tant qu'intrinsèque comme ceci:

double fsel( double a, double b, double c ) {
  return a >= 0 ? b : c; 
}

L'avantage est que cela se produit à l'intérieur du pipeline, sans créer de branche. En fait, si votre compilateur utilise l'intrinsèque, vous pouvez l'utiliser pour implémenter directement votre pince:

inline double clamp ( double a, double min, double max ) 
{
   a = fsel( a - min , a, min );
   return fsel( a - max, max, a );
}

Je vous suggère fortement d'éviter la manipulation de bits par les doubles en utilisant des opérations entières . Sur la plupart des processeurs modernes, il n’existe aucun moyen direct de transférer des données entre les registres double et int autrement qu’en effectuant un aller-retour vers le cache. Cela entraînera un risque de données appelé «load-hit-store» qui vide le pipeline de l'unité centrale jusqu'à ce que l'écriture en mémoire soit terminée (généralement environ 40 cycles). 

L'exception à cette règle est que les valeurs doubles sont déjà en mémoire et non dans un registre: dans ce cas, il n'y a pas de danger de load-hit-store. Cependant, votre exemple indique que vous venez de calculer le double et de le renvoyer à partir d'une fonction, ce qui signifie qu'il est susceptible de l'être encore en XMM1.

14
Crashworks

Plutôt que de tester et de créer des branches, j'utilise normalement ce format pour le bridage:

clampedA = fmin(fmax(a,MY_MIN),MY_MAX);

Bien que je n'ai jamais fait d'analyse de performance sur le code compilé.

7
Linasses

Les bits de la virgule flottante IEEE 754 sont ordonnés de manière à ce que, si vous comparez les bits interprétés comme un entier, vous obteniez les mêmes résultats que si vous les compariez directement en tant que flottants. Ainsi, si vous trouvez ou connaissez un moyen de bloquer des entiers, vous pouvez également l'utiliser pour les flottants (IEEE 754). Désolé, je ne connais pas de moyen plus rapide.

Si vous avez les flottants stockés dans des tableaux, vous pouvez envisager d’utiliser des extensions de processeur telles que SSE3, comme le dit rkj. Vous pouvez jeter un oeil à liboil, il fait tout le sale boulot pour vous. Garde votre programme portable et utilise des instructions plus rapides du processeur, si possible. (Je ne suis pas sûr que liboil soit indépendant de l'OS/du compilateur).

7
quinmars

En réalité, aucun compilateur décent ne fera la différence entre une instruction if () et une expression?:. Le code est assez simple pour qu'ils puissent identifier les chemins possibles. Cela dit, vos deux exemples ne sont pas identiques. Le code équivalent utilisant?: Serait

a = (a > MAX) ? MAX : ((a < MIN) ? MIN : a);

évitant ainsi le test A <MIN lorsqu’un> MAX. Cela pourrait faire une différence, sinon le compilateur devrait repérer la relation entre les deux tests.

Si le serrage est rare, vous pouvez tester la nécessité de serrer avec un seul test:

if (abs(a - (MAX+MIN)/2) > ((MAX-MIN)/2)) ...

Par exemple. avec MIN = 6 et MAX = 10, cela décale d’abord de 8 puis vérifie si elle est comprise entre -2 et +2. Que cela économise quoi que ce soit dépend beaucoup du coût relatif de la création de branches.

4
MSalters

Voici une implémentation probablement plus rapide similaire à celle de @ Roddy :

typedef int64_t i_t;
typedef double  f_t;

static inline
i_t i_tmin(i_t x, i_t y) {
  return (y + ((x - y) & -(x < y))); // min(x, y)
}

static inline
i_t i_tmax(i_t x, i_t y) {
  return (x - ((x - y) & -(x < y))); // max(x, y)
}

f_t clip_f_t(f_t f, f_t fmin, f_t fmax)
{
#ifndef TERNARY
  assert(sizeof(i_t) == sizeof(f_t));
  //assert(not (fmin < 0 and (f < 0 or is_negative_zero(f))));
  //XXX assume IEEE-754 compliant system (lexicographically ordered floats)
  //XXX break strict-aliasing rules
  const i_t imin = *(i_t*)&fmin;
  const i_t imax = *(i_t*)&fmax;
  const i_t i    = *(i_t*)&f;
  const i_t iclipped = i_tmin(imax, i_tmax(i, imin));

#ifndef INT_TERNARY
  return *(f_t *)&iclipped;
#else /* INT_TERNARY */
  return i < imin ? fmin : (i > imax ? fmax : f); 
#endif /* INT_TERNARY */

#else /* TERNARY */
  return fmin > f ? fmin : (fmax < f ? fmax : f);
#endif /* TERNARY */
}

Voir Calculer le minimum (min) ou le maximum (max) de deux entiers sans ramification et Comparaison de nombres en virgule flottante

Les formats flottants et doubles de l'IEEE étaient conçu pour que les nombres soient «Ordonné lexicographiquement», qui – dans les mots de l'architecte de l'IEEE William Kahan signifie “si deux en virgule flottante les nombres dans le même format sont commandés (dites x <y), alors ils sont commandés de la même manière quand leurs bits sont réinterprété en tant que Sign-Magnitude entiers. "

Un programme de test:

/** gcc -std=c99 -fno-strict-aliasing -O2 -lm -Wall *.c -o clip_double && clip_double */
#include <assert.h> 
#include <iso646.h>  // not, and
#include <math.h>    // isnan()
#include <stdbool.h> // bool
#include <stdint.h>  // int64_t
#include <stdio.h>

static 
bool is_negative_zero(f_t x) 
{
  return x == 0 and 1/x < 0;
}

static inline 
f_t range(f_t low, f_t f, f_t hi) 
{
  return fmax(low, fmin(f, hi));
}

static const f_t END = 0./0.;

#define TOSTR(f, fmin, fmax, ff) ((f) == (fmin) ? "min" :       \
                  ((f) == (fmax) ? "max" :      \
                   (is_negative_zero(ff) ? "-0.":   \
                    ((f) == (ff) ? "f" : #f))))

static int test(f_t p[], f_t fmin, f_t fmax, f_t (*fun)(f_t, f_t, f_t)) 
{
  assert(isnan(END));
  int failed_count = 0;
  for ( ; ; ++p) {
    const f_t clipped  = fun(*p, fmin, fmax), expected = range(fmin, *p, fmax);
    if(clipped != expected and not (isnan(clipped) and isnan(expected))) {
      failed_count++;
      fprintf(stderr, "error: got: %s, expected: %s\t(min=%g, max=%g, f=%g)\n", 
          TOSTR(clipped,  fmin, fmax, *p), 
          TOSTR(expected, fmin, fmax, *p), fmin, fmax, *p);
    }
    if (isnan(*p))
      break;
  }
  return failed_count;
}  

int main(void)
{
  int failed_count = 0;
  f_t arr[] = { -0., -1./0., 0., 1./0., 1., -1., 2, 
        2.1, -2.1, -0.1, END};
  f_t minmax[][2] = { -1, 1,  // min, max
               0, 2, };

  for (int i = 0; i < (sizeof(minmax) / sizeof(*minmax)); ++i) 
    failed_count += test(arr, minmax[i][0], minmax[i][1], clip_f_t);      

  return failed_count & 0xFF;
}

En console:

$ gcc -std=c99 -fno-strict-aliasing -O2 -lm *.c -o clip_double && ./clip_double 

Il imprime:

error: got: min, expected: -0.  (min=-1, max=1, f=0)
error: got: f, expected: min    (min=-1, max=1, f=-1.#INF)
error: got: f, expected: min    (min=-1, max=1, f=-2.1)
error: got: min, expected: f    (min=-1, max=1, f=-0.1)
2
jfs

J'ai essayé l'approche SSE à ce sujet moi-même, et la sortie de Assembly avait l'air un peu plus propre. J'étais donc encouragée au début, mais après un calcul minutieux des milliers de fois, elle était en réalité un peu plus lente. Il semble en effet que le compilateur VC++ n’est pas assez intelligent pour savoir ce que vous voulez vraiment, et il semble bouger les choses entre les registres XMM et la mémoire alors qu’il ne le devrait pas. Cela dit, je ne sais pas pourquoi le compilateur n'est pas assez intelligent pour utiliser les instructions SSE min/max de l'opérateur ternaire alors qu'il semble utiliser les instructions SSE pour tous les calculs en virgule flottante. Par contre, si vous compilez pour PowerPC, vous pouvez utiliser le fsel intrinsèque sur les registres FP, et le processus sera bien plus rapide.

1
Corey

Comme indiqué ci-dessus, les fonctions fmin/fmax fonctionnent bien (en gcc, avec -ffast-math). Bien que gfortran ait des motifs d'utilisation des instructions IA correspondant à max/min, ce n'est pas le cas de g ++. Dans icc, il faut utiliser std :: min/max, car icc ne permet pas de raccourcir la spécification du fonctionnement de fmin/fmax avec des opérandes non finis.

0
tim18

Si vous voulez utiliser des instructions rapides de valeur absolue, consultez ce fragment de code que j'ai trouvé dans mini-ordinateur , qui fixe un flottant dans la plage [0,1]

clamped = 0.5*(fabs(x)-fabs(x-1.0f) + 1.0f);

(J'ai simplifié un peu le code). On peut penser qu’il prend deux valeurs, l’une étant> 0

fabs(x)

et l'autre reflète environ 1,0 à <1,0

1.0-fabs(x-1.0)

Et nous prenons la moyenne d'entre eux. Si elle est dans la plage, les deux valeurs seront identiques à x, leur moyenne sera donc à nouveau x. Si elle est en dehors de la plage, alors l'une des valeurs sera x, et l'autre sera x retournée sur le point "limite", ainsi leur moyenne sera précisément le point limite.

0
Jeremy Salwen

Mes 2 centimes en C++. Probablement pas différent d'utiliser des opérateurs ternaires et nous espérons qu'aucun code de branchement n'est généré

template <typename T>
inline T clamp(T val, T lo, T hi) {
    return std::max(lo, std::min(hi, val));
}
0
wcochran

Si je comprends bien, vous voulez limiter la valeur "a" à une plage comprise entre MY_MIN et MY_MAX. Le type de "a" est un double. Vous n'avez pas spécifié le type de MY_MIN ou MY_MAX.

La simple expression:

clampedA = (a > MY_MAX)? MY_MAX : (a < MY_MIN)? MY_MIN : a;

devrait faire l'affaire.

Je pense qu'il peut y avoir une petite optimisation à faire si MY_MAX et MY_MIN se trouvent être des entiers:

int b = (int)a;
clampedA = (b > MY_MAX)? (double)MY_MAX : (b < MY_MIN)? (double)MY_MIN : a;

En passant aux comparaisons d’entiers, il est possible que vous obteniez un léger avantage en termes de vitesse.

0
abelenky

Je pense que vous pourriez utiliser SSE3 ou une technologie similaire pour cela, mais je ne sais pas exactement quelles commandes/comment ... Vous pouvez consulter: Arithmétique de saturation

0
rkj