web-dev-qa-db-ja.com

フロートとダブルの2による高速乗算/除算(C / C ++)

私が書いているソフトウェアでは、値の2(または2の累乗)で数百万の乗算または除算を行っています。ビットシフト演算子にアクセスできるように、これらの値をintにしたいと思います。

_int a = 1;
int b = a<<24
_

しかし、私はできません、そして私はダブルスに固執しなければなりません。

私の質問は:double(符号、指数、仮数)の標準表現があるので、2の累乗で高速の乗算/除算を取得するために指数で遊ぶ方法はありますか?

ビット数が固定されると想定することもできます(ソフトウェアは、常に64ビット長のdoubleを持つマシンで動作します)

追伸:はい、アルゴリズムはほとんどこれらの操作のみを実行します。これがボトルネックです(すでにマルチスレッド化されています)。

編集:または私は完全に間違っていて、賢いコンパイラはすでに私のために物事を最適化していますか?


一時的な結果(時間を測定するためのQt、やり過ぎですが、私は気にしません):

_#include <QtCore/QCoreApplication>
#include <QtCore/QElapsedTimer>
#include <QtCore/QDebug>

#include <iostream>
#include <math.h>

using namespace std;

int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);

while(true)
{
    QElapsedTimer timer;
    timer.start();

    int n=100000000;
    volatile double d=12.4;
    volatile double D;
    for(unsigned int i=0; i<n; ++i)
    {
        //D = d*32;      // 200 ms
        //D = d*(1<<5);  // 200 ms
        D = ldexp (d,5); // 6000 ms
    }

    qDebug() << "The operation took" << timer.elapsed() << "milliseconds";
}

return a.exec();
}
_

実行は、D = d*(1<<5);と_D = d*32;_が同時に実行される(200ミリ秒)のに対し、D = ldexp (d,5);ははるかに遅い(6000ミリ秒)ことを示しています。 I knowこれはマイクロベンチマークであり、突然、RAMが突然Piの計算を要求したため、私のChromeが爆発しましたldexp()を実行するたびに背中にあるので、このベンチマークは何の価値もありませんが、それでも維持します。

一方、const違反があるため、_reinterpret_cast<uint64_t *>_の実行に問題があります(volatileキーワードが干渉しているようです)

25
Fezvez

IEEE 754フォーマットをかなり安全に想定できますが、その詳細はかなり厄介になる可能性があります(特に非正規化数に入るとき)。ただし、一般的なケースでは、これは機能するはずです。

const int DOUBLE_EXP_SHIFT = 52;
const unsigned long long DOUBLE_MANT_MASK = (1ull << DOUBLE_EXP_SHIFT) - 1ull;
const unsigned long long DOUBLE_EXP_MASK = ((1ull << 63) - 1) & ~DOUBLE_MANT_MASK; 
void unsafe_shl(double* d, int shift) { 
    unsigned long long* i = (unsigned long long*)d; 
    if ((*i & DOUBLE_EXP_MASK) && ((*i & DOUBLE_EXP_MASK) != DOUBLE_EXP_MASK)) { 
        *i += (unsigned long long)shift << DOUBLE_EXP_SHIFT; 
    } else if (*i) {
        *d *= (1 << shift);
    }
} 

編集:いくつかのタイミングを実行した後、このメソッドは、コンパイラとマシンのdoubleメソッドよりも奇妙に遅くなり、実行される最小のコードまで削除されます。

    double ds[0x1000];
    for (int i = 0; i != 0x1000; i++)
        ds[i] = 1.2;

    clock_t t = clock();

    for (int j = 0; j != 1000000; j++)
        for (int i = 0; i != 0x1000; i++)
#if DOUBLE_SHIFT
            ds[i] *= 1 << 4;
#else
            ((unsigned int*)&ds[i])[1] += 4 << 20;
#endif

    clock_t e = clock();

    printf("%g\n", (float)(e - t) / CLOCKS_PER_SEC);

DOUBLE_SHIFTでは1.6秒で完了し、内部ループは

movupd xmm0,xmmword ptr [ecx]  
lea    ecx,[ecx+10h]  
mulpd  xmm0,xmm1  
movupd xmmword ptr [ecx-10h],xmm0

それ以外の場合は2.4秒、内部ループは次のとおりです。

add dword ptr [ecx],400000h
lea ecx, [ecx+8]  

本当に意外!

編集2:謎が解けた! VC11の変更点の1つは、常に浮動小数点ループをベクトル化し、/ Arch:SSE2を効果的に強制することです。ただし、VC10は、/ Arch:SSE2を使用しても、3.0秒でさらに悪化し、内部ループは次のようになります。

movsd xmm1,mmword ptr [esp+eax*8+38h]  
mulsd xmm1,xmm0  
movsd mmword ptr [esp+eax*8+38h],xmm1  
inc   eax

VC10without/ Arch:SSE2(/ Arch:SSEがあっても)は5.3秒です... 1/100の反復で! !、内部ループ:

fld         qword ptr [esp+eax*8+38h]  
inc         eax  
fmul        st,st(1)  
fstp        qword ptr [esp+eax*8+30h]

X87 FPスタックが素晴らしいことは知っていましたが、500倍悪いのはちょっとばかげています。これはSSEスタックにロードし、1つの操作を実行し、そこから格納する最悪のケースであるため、これらの種類の高速化、つまり行列演算からFPまたはintハックへの変換はおそらく見られません。しかし、これはx87がパフォーマンスを向上させる方法ではない理由の良い例です。関連。

8
Simon Buchan

これは、アプリケーション固有のものの1つです。役立つ場合もあれば、役に立たない場合もあります。 (ほとんどの場合、単純な乗算が依然として最適です。)

これを行う「直感的な」方法は、ビットを64ビット整数に抽出し、シフト値を指数に直接追加することです。 (これは、NANまたはINFをヒットしない限り機能します)

だからこのようなもの:

union{
    uint64 i;
    double f;
};

f = 123.;
i += 0x0010000000000000ull;

//  Check for zero. And if it matters, denormals as well.

このコードはCに準拠しておらず、アイデアを説明するためだけに示されていることに注意してください。これを実装する試みは、アセンブリまたはSSE組み込み関数。で直接行う必要があります。

ただし、mostの場合、FP単位から整数単位(およびその逆)は、単に乗算を完全に行うよりもはるかにコストがかかります。これは、値をx87 FPUからメモリに格納してから、読み戻す必要があるSSE以前の時代に特に当てはまります。整数レジスタに。

SSEの時代では、整数SSEおよびFP SSE同じものを使用しますISAレジスタ(まだ個別のレジスタファイルがあります) Agner Fog によると、整数間でデータを移動すると1〜2サイクルのペナルティがありますSSEおよびFP SSE実行単位。したがって、コストはx87時代よりもはるかに優れていますが、それでも問題はありません。

全体として、それはあなたがあなたのパイプラインに他に何を持っているかに依存します。しかし、ほとんどの場合、乗算はさらに高速になります。私は以前にこれとまったく同じ問題に遭遇したことがあるので、私は直接の経験から話しています。

FP命令のみをサポートする256ビットAVX命令を使用すると、このようなトリックを実行するインセンティブはさらに少なくなります。

18
Mysticial

ldexp はどうですか?

中途半端なコンパイラは、プラットフォーム上で最適なコードを生成します。

しかし、@ Clintonが指摘しているように、単に「明白な」方法でそれを書くことも同様に機能するはずです。 2の累乗で乗算および除算することは、現代のコンパイラーにとっては子供の遊びです。

浮動小数点表現を直接変更することは、移植性がないことに加えて、ほぼ確実に速くはなりません(そして遅くなる可能性があります)。

そしてもちろん、プロファイリングツールから指示がない限り、この質問について考えても時間を無駄にしないでください。しかし、このアドバイスを聞くような人は決してそれを必要とせず、それを必要とする人は決してそれを聞くことはありません。

[更新]

OK、それで私はちょうどg ++ 4.5.2でldexpを試しました。 cmathヘッダーは、__builtin_ldexpへの呼び出しとしてインライン化します。

... libm ldexp関数の呼び出しを発行します。このビルトインを最適化するのは簡単だと思っていたでしょうが、GCC開発者はそれを実現できなかったと思います。

したがって、あなたが発見したように、1 << pを掛けることはおそらくあなたの最善の策です。

8
Nemo

これを行う最も速い方法はおそらく次のとおりです。

x *= (1 << p);

この種のことは、マシン命令を呼び出してpを指数に追加することで簡単に実行できます。代わりにマスクを使用していくつかのビットを抽出するようにコンパイラーに指示し、手動で何かを実行すると、速度が遅くなる可能性があります。

C/C++はアセンブリ言語ではないことを忘れないでください。ビットシフト演算子を使用しても、必ずしもビットシフトアセンブリ操作にコンパイルされるとは限りません。乗算を使用すると、必ずしも乗算にコンパイルされるとは限りません。どのレジスタが使用されているか、どの命令を同時に実行できるかなど、さまざまな奇妙で素晴らしいことが起こっていますが、私には理解できません。しかし、何年にもわたる知識と経験、そして多くの計算能力を備えたコンパイラーは、これらの判断を下すのにはるかに優れています。

ps doubleが配列またはその他のフラットなデータ構造にある場合、コンパイラは非常に賢く、SSEを複数の2に、またはさらには4は同時に倍増しますが、多くのビットシフトを行うと、コンパイラが混乱し、この最適化が妨げられる可能性があります。

5
Clinton

このアルゴリズムには他にどのような操作が必要ですか?フロートをintペア(符号/仮数と大きさ)に分割し、処理を実行して、最後にそれらを再構成できる場合があります。

1
Thom Smith

特にダブルタイプのフロートの場合、2の累乗を処理することには実際的な利点はほとんどないか、まったくありませんが、 ダブルダブル タイプの場合があります。ダブルダブルの乗算と除算は一般に複雑ですが、2の累乗で乗算と除算を行うのは簡単です。

例えば。にとって

typedef struct {double hi; double lo;} doubledouble;
doubledouble x;
x.hi*=2, x.lo*=2; //multiply x by 2
x.hi/=2, x.lo/=2; //divide x by 2

実際、私はdoubledouble<<>>をオーバーロードして、整数に類似させました。

//x is a doubledouble type
x << 2 // multiply x by four;
x >> 3 // divide x by eight.
1
Z boson

2を掛けると、加算に置き換えることができます。x *= 2x += xと同等です。

2による除算は、0.5による乗算に置き換えることができます。乗算は通常、除算よりも大幅に高速です。

1
Mark Ransom

乗算する対象によっては、十分に繰り返されるデータがある場合、メモリを犠牲にして、ルックアップテーブルのパフォーマンスが向上する可能性があります。

0
Kevin Guerra