web-dev-qa-db-ja.com

実際の(固定/浮動小数点)値をクランプする最も速い方法は?

Ifステートメントまたは三項演算子を使用するよりも実数をクランプするためのより効率的な方法はありますか?これをdoubleと32ビットのフィックスポイント実装(16.16)の両方で実行したいと考えています。私はnotの両方のケースを処理できるコードを求めています。それらは別の関数で処理されます。

明らかに、私は次のようなことができます:

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

または

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

フィックスポイントバージョンは、比較のために関数/マクロを使用します。

これはコードのパフォーマンスが重要な部分で行われるので、できる限り効率的な方法を探しています(ビット操作を伴うと思われます)。

編集:標準/ポータブルCである必要があります。プラットフォーム固有の機能はここでは対象外です。また、MY_MINおよびMY_MAXは、クランプしたい値と同じ型です(上記の例ではdoubleです)。

38
Niklas

16.16表現の場合、単純な3値は速度的に改善される可能性は低いです。

また、doubleの場合は、標準/ポータブルCが必要なため、あらゆる種類のビットいじりはひどく終わります。

ビットフィドルが可能だったとしても(疑わしいと思います)、doubleのバイナリ表現に依存することになります。これ(およびそのサイズ)IS実装に依存。

おそらく、sizeof(double)を使用してこれを「推測」し、さまざまなdouble値のレイアウトをそれらの一般的なバイナリ表現と比較することができますが、何も隠していないと思います。

最良のルールは、コンパイラに何を求めているか(3項)を伝え、最適化することです。

編集:控えめなパイの時間。私はquinmarsのアイデア(以下)をテストしましたが、IEEE-754 floatを使用している場合は機能します。これにより、以下のコードで約20%のスピードアップが得られました。明らかに移植不可能ですが、コンパイラが#IFでIEEE754浮動小数点形式を使用するかどうかを確認する標準化された方法があると思います...?

  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

GCCとclangはどちらも、次のシンプルでわかりやすい、移植可能なコード用の美しいアセンブリを生成します。

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

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

Clangで生成されたアセンブリ:

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

3つの命令(retを含まない)、分岐なし。優秀な。

これは、Core i3 M 350を搭載したUbuntu 13.04上のGCC 4.7およびclang 3.2でテストされました。補足として、std :: minおよびstd :: maxを呼び出す単純なC++コードは同じアセンブリを生成しました。

これはダブルス用です。また、intの場合、GCCとclangの両方が、5つの命令(retは数えません)を持ち、分岐を含まないAssemblyを生成します。また素晴らしい。

私は現在固定小数点を使用していないので、固定小数点については意見を述べません。

37
Jorge

古い質問ですが、今日この問題に取り組んでいました(doubles/floatsを使用)。

最善の方法は、SSE floatにはMINSS/MAXSSを使用し、doubleにはSSE2 MINSD/MAXSDを使用することです。これらはブランチなしで、それぞれ1クロックサイクルかかり、コンパイラ組み込み関数のおかげで使いやすいです。 std :: min/maxを使用したクランプと比較して、パフォーマンスが1桁以上向上します。

あなたはそれを意外に感じるかもしれません。確かに!残念ながら、VC++ 2010では、/ Arch:SSE2および/ FP:fastが有効になっている場合でも、std :: min/maxの単純な比較が使用されます。他のコンパイラについて話すことはできません。

これは、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;
}

倍精度コードは、xxx_sdを除いて同じです。

編集:最初はコメント付きでクランプ関数を記述しました。しかし、アセンブラーの出力を見ると、VC++コンパイラーは冗長な動きを間引くほどスマートではないことがわかりました。 1つ少ない命令。 :)

37
Spat

プロセッサに(x86のように)絶対値の高速命令がある場合、ifステートメントまたは3項演算よりも高速なブランチなしの最小値と最大値を実行できます。

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

項の1つがゼロである場合(クランプしている場合によくあることです)、コードはさらに簡単になります。

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

両方の操作を組み合わせる場合、2つの/2を1つの/4または*0.25に置き換えて、ステップを保存できます。

次のコードは、FMIN = 0の最適化を使用すると、私のAthlon II X2の3値より3倍以上高速です。

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
}
16
Mark Ransom

ほとんどのコンパイラーは、ブランチの代わりに条件付き移動を使用するネイティブハードウェア操作にそれらをコンパイルできるので、3項演算子が実際に進むべき道です(したがって、予測ミスのペナルティやパイプラインのバブルなどを回避します)。 ビット操作によりロードヒットストアが発生する可能性があります

特に、PPCおよびSSE2を搭載したx86には、次のような組み込みのものとして表現できるハードウェアopがあります。

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

利点は、分岐を発生させることなく、パイプライン内でこれを実行することです。実際、コンパイラがコンパイラ組み込み関数を使用している場合は、それを使用してクランプを直接実装できます。

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

整数演算を使用したdoubleのビット操作を避けるを強くお勧めします。最近のほとんどのCPUでは、dcacheに往復する以外に、doubleレジスタとintレジスタの間でデータを移動する直接的な方法はありません。これにより、メモリへの書き込みが完了するまで(通常は約40サイクル程度)、基本的にCPUパイプラインを空にするロードヒットストアと呼ばれるデータハザードが発生します。

これの例外は、double値がすでにメモリ内にあり、レジスタ内にない場合です。その場合、ロードヒットストアの危険はありません。しかし、あなたの例は、あなたがちょうどdoubleを計算し、それを関数から返したことを示しています。

14
Crashworks

テストや分岐ではなく、通常はクランプにこの形式を使用します。

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

コンパイルされたコードでパフォーマンス分析を行ったことはありませんが。

7
Linasses

IEEE 754浮動小数点のビットは、整数として解釈されるビットを比較すると、浮動小数点として直接比較する場合と同じ結果が得られるように順序付けられています。したがって、整数をクランプする方法を見つけた、または知っている場合は、それを(IEEE 754)浮動小数点数にも使用できます。すみません、もっと速い方法はわかりません。

フロートを配列に格納している場合は、rkjが言っているように、SSE3のようないくつかのCPU拡張を使用することを検討できます。あなたはそれがあなたのためにすべての汚い仕事をするliboilを見てみることができます。プログラムを移植可能に保ち、可能であればより高速なCPU命令を使用します。 (OS /コンパイラに依存しないliboilがどのようになっているかはわかりません)。

7
quinmars

現実的には、まともなコンパイラーがif()ステートメントと?:式を区別しません。コードは非常に単純なので、可能なパスを見つけることができます。つまり、2つの例は同じではありません。 ?:を使用する同等のコードは

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

> MAXの場合、A <MINテストを回避します。コンパイラーが2つのテスト間の関係を特定する必要があるため、これで違いが生じる可能性があります。

クランプがまれな場合は、1回のテストでクランプの必要性をテストできます。

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

例えば。 MIN = 6およびMAX = 10の場合、これはまず8だけ下にシフトし、次に-2と+2の間にあるかどうかを確認します。これが何かを節約するかどうかは、分岐の相対的なコストに大きく依存します。

4
MSalters

@ 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 */
}

分岐せずに2つの整数の最小(min)または最大(max)を計算する および 浮動小数点数の比較 を参照してください。

IEEEの浮動小数点形式と倍精度形式は、数値が「辞書式順序付け」されるように設計されています。これは、IEEEアーキテクトのWilliam Kahanの言葉では、「同じ形式の2つの浮動小数点数が順序付けられている場合(たとえば、x <y)、これらのビットは、Sign-Magnitude整数として再解釈される場合と同じ方法で順序付けされます。」

テストプログラム:

/** 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;
}

コンソールで:

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

それは印刷します:

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

私はこれにSSEアプローチを試してみましたが、アセンブリの出力はかなりきれいに見えたので、最初は励まされましたが、数千回のタイミングの後、実際にはかなり遅くなりました確かに、VC++コンパイラーは実際に何を意図しているのかを知るのに十分なほどスマートではなく、XMMレジスターとメモリーの間を行き来しないように移動するように見えます。すべての浮動小数点に対してSSE命令を使用しているように見える場合に、コンパイラが三項演算子でSSE min/max命令を使用するほどスマートではない理由を知るとにかくポイント計算。一方、PowerPC用にコンパイルしている場合は、FPレジスタでfselコンパイラ組み込み関数を使用できます。

1
Corey

上記で指摘したように、fmin/fmax関数は(gccでは-ffast-mathで)うまく機能します。 gfortranには、max/minに対応するIA命令を使用するパターンがありますが、g ++にはありません。 iccでは、代わりにstd :: min/maxを使用する必要があります。これは、iccが非有限オペランドでのfmin/fmaxの動作方法の指定をショートカットすることを許可していないためです。

0
tim18

高速な絶対値命令を使用したい場合は、浮動小数点数を[0,1]の範囲にクランプする minicomputer で見つけたコードの一部を確認してください。

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

(私はコードを少し簡略化しました)。 2つの値を取ると考えることができ、1つは0より大きいと反映されます。

fabs(x)

もう一方は1.0未満であり、<1.0

1.0-fabs(x-1.0)

そして、それらの平均を取ります。範囲内にある場合、両方の値はxと同じになるため、それらの平均は再びxになります。範囲外の場合、値の1つはxになり、もう1つは「境界」ポイント上でx反転されるため、それらの平均は正確に境界ポイントになります。

0
Jeremy Salwen

C++で私の2セント。おそらく、三項演算子を使用することと何ら違いはなく、うまくいけば分岐コードは生成されません

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

私が正しく理解している場合は、値 "a"をMY_MINからMY_MAXの範囲に制限する必要があります。 「a」のタイプはdoubleです。 MY_MINまたはMY_MAXのタイプを指定していません。

単純な式:

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

トリックを行う必要があります。

MY_MAXとMY_MINが整数の場合、小さな最適化が行われる可能性があると思います。

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

整数比較に変更すると、速度がわずかに向上する可能性があります。

0
abelenky