web-dev-qa-db-ja.com

文字の配列がゼロかどうかをすばやく確認する方法

メモリ内にバイトの配列があります。配列内のすべてのバイトがゼロであるかどうかを確認する最も速い方法は何ですか?

19
Claudiu

今日では、使用不足 [〜#〜] simd [〜#〜] 拡張命令[〜#〜] sse [〜#〜]など x86プロセッサの場合)配列を反復処理そして各値を0と比較することもできます。

遠い過去、配列内の各要素(ループ分岐自体に加えて)に対して比較と条件分岐を実行することは、費用がかかると見なされ、頻度(または初期)に応じて可能でした。ゼロ以外の要素が配列に表示されることを期待している場合は、完全にループ内の条件なしで実行、ビット単位のみを使用して-または設定されたビットを検出し、実際のチェックを終了するまで延期することを選択した可能性があります。ループが完了します:

int sum = 0;
for (i = 0; i < ARRAY_SIZE; ++i) {
  sum |= array[i];
}
if (sum != 0) {
  printf("At least one array element is non-zero\n");
}

ただし、今日のパイプライン化されたスーパースカラープロセッサの設計は 分岐予測 で完了しているため、SSE以外のすべてのアプローチはループ内で実質的に区別できません。どちらかといえば、各要素をゼロと比較し、(最初のゼロ以外の要素が検出されるとすぐに)ループから早く抜け出すことは、長期的には、sum |= array[i]アプローチ(常にトラバースする)よりも効率的である可能性があります。配列全体)つまり、配列がほとんど常にゼロだけで構成されていると予想される場合を除きます(この場合、GCCのsum |= array[i]を使用して-funroll-loopsアプローチを真にブランチレスにすると、より良い数値が得られます。 --Athlonプロセッサについては、以下の数値を参照してください。結果はプロセッサのモデルとメーカーによって異なる場合があります。)

#include <stdio.h>

int a[1024*1024];

/* Methods 1 & 2 are equivalent on x86 */  

int main() {
  int i, j, n;

# if defined METHOD3
  int x;
# endif

  for (i = 0; i < 100; ++i) {
#   if defined METHOD3
    x = 0;
#   endif
    for (j = 0, n = 0; j < sizeof(a)/sizeof(a[0]); ++j) {
#     if defined METHOD1
      if (a[j] != 0) { n = 1; }
#     Elif defined METHOD2
      n |= (a[j] != 0);
#     Elif defined METHOD3
      x |= a[j];
#     endif
    }
#   if defined METHOD3
    n = (x != 0);
#   endif

    printf("%d\n", n);
  }
}

$ uname -mp
i686 athlon
$ gcc -g -O3 -DMETHOD1 test.c
$ time ./a.out
real    0m0.376s
user    0m0.373s
sys     0m0.003s
$ gcc -g -O3 -DMETHOD2 test.c
$ time ./a.out
real    0m0.377s
user    0m0.372s
sys     0m0.003s
$ gcc -g -O3 -DMETHOD3 test.c
$ time ./a.out
real    0m0.376s
user    0m0.373s
sys     0m0.003s

$ gcc -g -O3 -DMETHOD1 -funroll-loops test.c
$ time ./a.out
real    0m0.351s
user    0m0.348s
sys     0m0.003s
$ gcc -g -O3 -DMETHOD2 -funroll-loops test.c
$ time ./a.out
real    0m0.343s
user    0m0.340s
sys     0m0.003s
$ gcc -g -O3 -DMETHOD3 -funroll-loops test.c
$ time ./a.out
real    0m0.209s
user    0m0.206s
sys     0m0.003s
27
vladr

インラインアセンブリを使用しても問題がない場合は、短くて簡単な解決策を次に示します。

#include <stdio.h>

int main(void) {
    int checkzero(char *string, int length);
    char str1[] = "wow this is not zero!";
    char str2[] = {0, 0, 0, 0, 0, 0, 0, 0};
    printf("%d\n", checkzero(str1, sizeof(str1)));
    printf("%d\n", checkzero(str2, sizeof(str2)));
}

int checkzero(char *string, int length) {
    int is_zero;
    __asm__ (
        "cld\n"
        "xorb %%al, %%al\n"
        "repz scasb\n"
        : "=c" (is_zero)
        : "c" (length), "D" (string)
        : "eax", "cc"
    );
    return !is_zero;
}

Assemblyに慣れていない場合は、ここで何を行うかを説明します。文字列の長さをレジスタに格納し、プロセッサに文字列のゼロをスキャンするように依頼します(これは下位8ビットを設定して指定します)アキュムレータの、すなわち%%al、ゼロ)、ゼロ以外のバイトが検出されるまで、各反復で上記のレジスタの値を減らします。これで、文字列がすべてゼロの場合、レジスタもlength回デクリメントされたため、ゼロになります。ただし、ゼロ以外の値が検出された場合、ゼロをチェックする「ループ」が途中で終了したため、レジスタはゼロになりません。次に、そのレジスタの値を取得し、そのブール否定を返します。

これをプロファイリングすると、次の結果が得られました。

$ time or.exe

real    0m37.274s
user    0m0.015s
sys     0m0.000s


$ time scasb.exe

real    0m15.951s
user    0m0.000s
sys     0m0.046s

(両方のテストケースは、サイズ100000の配列で100000回実行されました。or.exeコードはVladの答えから来ています。どちらの場合も、関数呼び出しは削除されました。)

12
susmits

これを32ビットCで実行する場合は、おそらく配列を32ビット整数配列としてループして0と比較し、最後にあるものも0であることを確認します。

4
WhirlWind

チェックされたメモリを半分に分割し、最初の部分を2番目の部分と比較します。
a。違いがあるとしても、すべて同じにすることはできません。
b。違いがなければ、前半を繰り返します。

最悪の場合2 * N。メモリ効率が高く、memcmpベース。
実際に使用すべきかどうかはわかりませんが、自己比較のアイデアは気に入りました。
奇数の長さで機能します。理由がわかりますか? :-)

bool memcheck(char* p, char chr, size_t size) {
    // Check if first char differs from expected.
    if (*p != chr) 
        return false;
    int near_half, far_half;
    while (size > 1) {
        near_half = size/2;
        far_half = size-near_half;
        if (memcmp(p, p+far_half, near_half))
            return false;
        size = far_half;
    }
    return true;
}
3
Kobor42

配列のサイズが適切な場合、最新のCPUの制限要因はメモリへのアクセスになります。

__dcbtやprefetchnta(または、すぐにバッファを再度使用する場合はprefetch0)のようなものを使用して、適切な距離(1〜2K)先のキャッシュプリフェッチを使用してください。

また、SIMDやSWARのようなことを、一度に複数のバイトに対して実行することもできます。 32ビットワードの場合でも、文字ごとのバージョンの4分の1の操作になります。 orを展開し、orの「ツリー」にフィードさせることをお勧めします。私のコード例で私が何を意味するかを見ることができます-これは、中間データの依存関係がそれほど多くないopsを利用することにより、2つの整数演算(or's)を並行して実行するスーパースカラー機能を利用します。私は8のツリーサイズ(4x4、2x2、1x1)を使用していますが、CPUアーキテクチャにある空きレジスタの数に応じて、それをより大きな数に拡張できます。

次の内部ループ(プロローグ/エピローグなし)の擬似コードの例では、32ビットのintを使用していますが、MMX/SSEまたは使用可能なものなら何でも64/128ビットを実行できます。ブロックをキャッシュにプリフェッチした場合、これはかなり高速になります。また、バッファが4バイトにアラインされていない場合は前に、バッファ(アライン後)の長さが32バイトの倍数でない場合は、アラインされていないチェックを実行する必要があります。

const UINT32 *pmem = ***aligned-buffer-pointer***;

UINT32 a0,a1,a2,a3;
while(bytesremain >= 32)
{
    // Compare an aligned "line" of 32-bytes
    a0 = pmem[0] | pmem[1];
    a1 = pmem[2] | pmem[3];
    a2 = pmem[4] | pmem[5];
    a3 = pmem[6] | pmem[7];
    a0 |= a1; a2 |= a3;
    pmem += 8;
    a0 |= a2;
    bytesremain -= 32;
    if(a0 != 0) break;
}

if(a0!=0) then ***buffer-is-not-all-zeros***

実際には、値の「行」の比較を1つの関数にカプセル化し、キャッシュのプリフェッチを使用してそれを数回展開することをお勧めします。

3
Adisak

ARM64で2つの実装を測定しました。1つはfalseで早期に戻るループを使用し、もう1つはすべてのバイトをORします。

int is_empty1(unsigned char * buf, int size)
{
    int i;
    for(i = 0; i < size; i++) {
        if(buf[i] != 0) return 0;
    }
    return 1;
}

int is_empty2(unsigned char * buf, int size)
{
    int sum = 0;
    for(int i = 0; i < size; i++) {
        sum |= buf[i];
    }
    return sum == 0;
}

結果:

すべての結果(マイクロ秒単位):

        is_empty1   is_empty2
MEDIAN  0.350       3.554
AVG     1.636       3.768

誤った結果のみ:

        is_empty1   is_empty2
MEDIAN  0.003       3.560
AVG     0.382       3.777

真の結果のみ:

        is_empty1   is_empty2
MEDIAN  3.649       3,528
AVG     3.857       3.751

Summary:誤った結果の確率が非常に小さいデータセットの場合のみ、ブランチが省略されているため、ORingを使用する2番目のアルゴリズムのパフォーマンスが向上します。そうでなければ、早期に戻ることは明らかに優れた戦略です。

2
Ortwin Gentz

RustyRusselのmemeqzerovery高速です。 memcmpを再利用して、手間のかかる作業を行います: https://github.com/rustyrussell/ccan/blob/master/ccan/mem/mem.c#L92

0
zbyszek