42

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

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

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_MINMY_MAXは、クランプしたい値と同じタイプです (上記の例では double)。

4

14 に答える 14

53

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 を数えない) でアセンブリを生成し、分岐はありません。また、優れています。

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

于 2013-05-20T22:18:08.387 に答える
40

古い質問ですが、今日はこの問題に取り組んでいました(ダブルス/フロートを使用)。

最善の方法は、float には SSE 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 つ減ります。:)

于 2011-07-03T22:52:14.173 に答える
18

プロセッサに絶対値の高速命令がある場合 (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
}
于 2011-09-15T01:26:50.710 に答える
14

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

特に、SSE2 を使用する PPC と 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 を計算して関数から返したことを示しています。これは、まだ XMM1 にある可能性が高いことを意味します。

于 2009-01-17T11:30:47.343 に答える
9

16.16 表現の場合、単純な 3 項が速度的に向上する可能性は低いです。

また、double の場合は、標準/移植可能な C が必要なため、あらゆる種類のビット操作がひどく終了します。

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

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

最良のルールは、コンパイラに何を望むかを伝え (つまり、3 進法)、最適化を任せることです。

編集:謙虚なパイの時間。quinmars のアイデア (以下) をテストしたところ、IEEE-754 フロートがあれば動作します。これにより、以下のコードが約 20% 高速化されました。明らかに移植性はありませんが、#IF を使用して IEEE754 float 形式を使用しているかどうかをコンパイラに問い合わせる標準化された方法があると思います...?

  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);
于 2009-01-09T10:09:18.477 に答える
7

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

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

于 2009-01-09T10:07:29.650 に答える
7

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

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

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

于 2010-03-21T15:23:19.313 に答える
4

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

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

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

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

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

たとえば、MIN=6 と MAX=10 の場合、これはまず a を 8 だけ下にシフトし、次に -2 と +2 の間にあるかどうかをチェックします。これで節約できるかどうかは、分岐の相対的なコストに大きく依存します。

于 2009-01-09T10:06:13.507 に答える
2

@Roddy's answerに似た、おそらくより高速な実装を次に示します。

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 の float および double 形式は、数値が「辞書式に順序付けられる」ように設計されています。これは、IEEE アーキテクトの William Kahan の言葉を借りれば、「同じ形式の 2 つの浮動小数点数が順序付けられている場合 ( x < y など)、それらのビットが符号-大きさの整数として再解釈されるとき、それらは同じ方法で順序付けられます。」</p>

テストプログラム:

/** 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)
于 2009-01-09T19:36:09.200 に答える
1

これに対する SSE アプローチを自分で試してみたところ、アセンブリの出力がかなりきれいに見えたので、最初は勇気づけられましたが、何千回も時間を計った後、実際にはかなり遅くなりました。確かに、VC++ コンパイラは、実際に何を意図しているのかを知るほどスマートではないように見えます。また、XMM レジスタとメモリの間を移動してはならないときに移動しているように見えます。とはいえ、とにかくすべての浮動小数点計算にSSE命令を使用しているように見えるのに、コンパイラが三項演算子でSSE最小/最大命令を使用するほど賢くない理由はわかりません。一方、PowerPC 用にコンパイルしている場合は、FP レジスタで fsel 組み込み関数を使用でき、はるかに高速です。

于 2011-08-19T17:39:20.610 に答える
1

上記で指摘したように、fmin/fmax 関数は (gcc では -ffast-math を使用して) うまく機能します。gfortran には max/min に対応する IA 命令を使用するパターンがありますが、g++ にはありません。icc では、代わりに std::min/max を使用する必要があります。これは、icc では、fmin/fmax が非有限オペランドでどのように機能するかの仕様を簡略化することができないためです。

于 2015-11-14T17:22:09.137 に答える
0

私の理解が正しければ、値 "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;

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

于 2009-01-09T09:21:45.517 に答える
0

高速な絶対値命令を使用したい場合は、ミニコンピューターで見つけたこの抜粋されたコードをチェックしてください。これは、フロートを [0,1] の範囲にクランプします。

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

(コードを少し単純化しました)。2 つの値を取ると考えることができ、一方は >0 と見なされます。

fabs(x)

もう1つは、約1.0が<1.0であることを反映しています

1.0-fabs(x-1.0)

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

于 2012-05-20T07:46:22.450 に答える