11

必ずしも 0 から 1 の範囲にあるとは限らない UV (2D テクスチャ座標) を処理する必要があるコードに取り組んでいます。例として、au コンポーネントが 1.2 の UV を取得することがあります。これを処理するために、次のようにしてタイリングを引き起こすラッピングを実装しています。

u -= floor(u)
v -= floor(v)

これを行うと、1.2 が目的の結果である 0.2 になります。また、-0.4 が 0.6 になるなどの負のケースも処理します。

ただし、フロアへのこれらの呼び出しはかなり遅いです。Intel VTune を使用してアプリケーションのプロファイリングを行いましたが、このフロア操作だけで膨大なサイクルを費やしています。

この問題についていくつかの背景を読んだ後、私は次の関数を思いつきました。これは少し高速ですが、まだ多くのことが望まれています (私はまだ型変換のペナルティなどを被っています)。

int inline fasterfloor( const float x ) { return x > 0 ? (int) x : (int) x - 1; }

インライン アセンブリで達成されるいくつかのトリックを見てきましたが、正確に正しく機能したり、速度が大幅に向上したりするものはありません。

この種のシナリオを処理するための秘訣を知っている人はいますか?

4

9 に答える 9

12

古い質問ですが、私はそれに出くわし、満足のいく回答が得られていないことに少し動揺しました。

TL;DR: これには、インライン アセンブリ、組み込み関数、またはその他の指定されたソリューションを*使用しないでください! 代わりに、高速/安全でない数学の最適化 (g++ では "-ffast-math -funsafe-math-optimizations -fno-math-errno") を使用してコンパイルします。floor() が非常に遅い理由は、キャストがオーバーフローした場合 (FLT_MAX はどのサイズのスカラー整数型にも適合しない)、グローバルな状態を変更するためです。これにより、厳密な IEEE-754 互換性を無効にしない限り、ベクトル化も不可能になります。とにかく、これはおそらく信頼すべきではありません。これらのフラグを使用してコンパイルすると、問題の動作が無効になります。

いくつかのコメント:

  1. スカラー レジスタを使用したインライン アセンブリはベクトル化できないため、最適化を使用してコンパイルすると、パフォーマンスが大幅に低下します。また、現在ベクトル レジスタに格納されている関連する値をスタックにスピルし、スカラー レジスタに再ロードする必要があるため、手作業による最適化の目的が無効になります。

  2. あなたが概説した方法でSSE cvttss2siを使用したインラインアセンブリは、コンパイラの最適化を使用した単純なforループよりも実際に私のマシンでは遅くなります。これは、コンパイラーがコードのブロック全体を一緒にベクトル化できるようにすると、レジスターが割り当てられ、パイプラインの停止が回避されるためと考えられます。内部依存チェーンがほとんどなく、レジスタ流出の可能性がほとんどない、このような短いコードの場合、asm() で囲まれた手作業で最適化されたコードよりも悪い結果になる可能性はほとんどありません。

  3. インライン アセンブリは移植性がなく、Visual Studio 64 ビット ビルドではサポートされておらず、非常に読みにくいものです。組み込み関数には、上記の警告と同じ警告があります。

  4. リストされている他のすべての方法は単に間違っています。これは間違いなく遅いことよりも悪いことであり、いずれの場合も、アプローチの粗さを正当化するほどのわずかなパフォーマンスの向上しか得られません。(int)(x+16.0)-16.0 はとても悪いので触れませんが、floor(-1) を -2 として与えるため、あなたの方法も間違っています。また、パフォーマンスが非常に重要であるため標準ライブラリでは処理できない場合に、数学コードに分岐を含めることは非常に悪い考えです。したがって、(間違った) 方法は ((int) x) - (x<0.0) のように見えるはずです。おそらく中間を使用するため、fpu の移動を 2 回実行する必要はありません。ブランチによってキャッシュ ミスが発生する可能性があり、パフォーマンスの向上が完全に無効になります。また、math errno が無効になっている場合、int へのキャストが floor() 実装の残りの最大のボトルネックになります。

  5. SUN の newlib 実装が fmodf で行うように、ビットごとのキャストとビットマスクによる丸めを使用してみましたが、適切に処理されるまでに非常に長い時間がかかり、関連するコンパイラ最適化フラグがなくても、私のマシンでは数倍遅くなりました。おそらく彼らは、浮動小数点演算が比較的非常に高価で、ベクトル変換演算はおろか、ベクトル拡張がなかった古代の CPU 用にそのコードを書きました。これは、知る限り、一般的なアーキテクチャには当てはまりません。SUN は、Quake 3 で使用される高速逆 sqrt() ルーチンの発祥の地でもあります。現在、ほとんどのアーキテクチャでそのための指示があります。マイクロ最適化の最大の落とし穴の 1 つは、すぐに古くなってしまうことです。

于 2017-01-10T03:17:46.163 に答える
12

本当に高速な float->int 変換が必要ですか? 私の知る限り、int-> float 変換は高速ですが、少なくとも MSVC++ では、float->int 変換は小さなヘルパー関数 ftol() を呼び出します。これは、標準に準拠した変換が行われるようにするためにいくつかの複雑なことを行います。そのような厳密な変換が必要ない場合は、x86 互換の CPU を使用していると仮定して、アセンブリ ハッカーを実行できます。

これは、MSVC++ インライン アセンブリ構文を使用して、float から int への高速な丸めを行う関数です (いずれにせよ正しいアイデアが得られるはずです)。

inline int ftoi_fast(float f)
{
    int i;

    __asm
    {
        fld f
        fistp i
    }

    return i;
}

MSVC++ 64 ビットでは、64 ビット コンパイラがインライン アセンブリを拒否するため、外部の .asm ファイルが必要になります。この関数は、基本的に生の x87 FPU 命令を使用して float をロードし (fld)、float を整数として格納します (fistp)。(警告: ここで使用されている丸めモードは、CPU のレジスタを直接調整することで変更できますが、そうしないでください。MSVC の sin と cos の実装を含め、多くのものを壊してしまいます!)

CPU で SSE がサポートされていると想定できる場合 (または、SSE をサポートするコードパスを作成する簡単な方法がある場合)、次のことも試すことができます。

#include <emmintrin.h>

inline int ftoi_sse1(float f)
{
    return _mm_cvtt_ss2si(_mm_load_ss(&f));     // SSE1 instructions for float->int
}

...これは基本的に同じ (float をロードしてから整数として格納) ですが、少し高速な SSE 命令を使用しています。

それらの 1 つは、高価な float から int へのケースをカバーする必要があり、int から float への変換は依然として安価である必要があります。ここでは Microsoft 固有で申し訳ありませんが、これは私が同様のパフォーマンス作業を行った場所であり、この方法で大きな利益を得ました. 移植性やその他のコンパイラが問題になる場合は、別のものを検討する必要がありますが、これらの関数は、100 クロック以上かかるヘルパー関数とは対照的に、5 クロック未満でおそらく 2 つの命令にコンパイルされます。

于 2010-02-28T21:34:17.953 に答える
3

必要な演算は、fmod関数(doubleではなくfloatの場合はfmodf)を使用して表現できます。

#include <math.h>
u = fmodf(u, 1.0f);

コンパイラがこれを最も効率的な方法で実行する可能性はかなりあります。

あるいは、ラストビットの精度についてどの程度懸念していますか?負の値が-16.0を下回らないことを知っている場合など、負の値に下限を設定できますか?もしそうなら、このようなものは条件付きを保存します。これは、データで確実にブランチ予測できるものでない場合に役立つ可能性が非常に高くなります。

u = (u + 16.0);  // Does not affect fractional part aside from roundoff errors.
u -= (int)u;     // Recovers fractional part if positive.

(さらに言えば、データの外観と使用しているプロセッサによっては、それらの大部分が負であるが、ごく一部が16.0未満である場合、条件付き整数を実行する前に16.0fを追加することがあります。キャストを使用すると、条件付きで予測可能になるため、スピードアップが得られます。または、コンパイラが条件付きブランチ以外の方法でそれを実行している場合、それは役に立ちません。生成されたアセンブリをテストして確認しないと、言うのは難しいです。)

于 2010-02-28T21:23:21.207 に答える
2

範囲が小さい場合にうまくいくかもしれない別のばかげたアイデア...

ビット演算を使用して float から指数を抽出し、ルックアップ テーブルを使用して、仮数から不要なビットを消去するマスクを見つけます。これを使用してフロアを見つけ(ポイントより下のビットをワイプ)、再正規化の問題を回避します。

編集私はこれを「ばかげている、プラス+ve対-veの問題」として削除しました。とにかく支持されたので、それは元に戻されており、それがどれほどばかげているかを判断するために他の人に任せます.

于 2010-02-28T19:53:53.873 に答える
2

Visual C++ を使用している場合は、「組み込み関数を有効にする」コンパイラ設定を確認してください。有効にすると、ほとんどの数学関数 (フロアを含む) が高速になります。欠点は、エッジ ケース (NaN など) の処理が正しくない可能性があることですが、ゲームの場合は気にしない場合があります。

于 2010-02-28T19:56:19.727 に答える
0

発生する可能性のある値の範囲が十分に小さい場合は、フロア値をバイナリ検索できます。たとえば、値 -2 <= x < 2 が発生する可能性がある場合...

if (u < 0.0)
{
  if (u < 1.0)
  {
    //  floor is 0
  }
  else
  {
    //  floor is 1
  }
}
else
{
  if (u < -1.0)
  {
    //  floor is -2
  }
  else
  {
    //  floor is -1
  }
}

これについては保証しません - 比較の効率がフロアとどのように比較されるかはわかりませんが、試してみる価値はあるかもしれません.

于 2010-02-28T19:45:51.020 に答える
0

これは鋳造コストを解決しませんが、数学的に正しいはずです:

int inline fasterfloor( const float x ) { return x < 0 ? (int) x == x ? (int) x : (int) x -1 : (int) x; }
于 2011-01-10T15:29:28.303 に答える
0

ループして u と v をインデックス座標として使用する場合は、float をフローリングして座標を取得する代わりに、float と int の両方を同じ値にして、それらを一緒にインクリメントします。これにより、必要なときに使用する対応する整数が得られます。

于 2014-04-23T15:07:09.090 に答える
0

u、v 値の最大入力範囲は? -5.0 から +5.0 のようにかなり小さい範囲の場合、floor などの高価な関数を呼び出すよりも、範囲内に収まるまで 1.0 を繰り返し加算/減算する方が速くなります。

于 2010-02-28T19:39:56.267 に答える