web-dev-qa-db-ja.com

配列内の要素を置換する高速な方法-C

次のようなintの配列があるとしましょう:

const int size = 100000;
int array[size];
//set some items to 0 and other items to 1

値が1のすべてのアイテムを、123456などの別の値に置き換えたいと思います。これは次のように簡単に実装できます。

for(int i = 0; i < size ; i++){
    if(array[i] != 0) 
        array[i] = 123456;
}

好奇心から、ある種のx86の策略によってこれを行うためのより速い方法がありますか、またはこれはプロセッサにとって最適なコードですか?

31
Axarydax

最初に0と1がある特定のケースでは、次のmightが高速になります。それをベンチマークする必要があります。ただし、プレーンCでこれ以上の改善を行うことはおそらくできないでしょう。存在する可能性のある「x86トリック」を利用したい場合は、アセンブリに飛び込む必要があります。

for(int i = 0; i < size ; i++){
  array[i] *= 123456;
}

編集:

ベンチマークコード:

#include <time.h>
#include <stdlib.h>
#include <stdio.h>

size_t diff(struct timespec *start, struct timespec *end)
{
  return (end->tv_sec - start->tv_sec)*1000000000 + end->tv_nsec - start->tv_nsec;
}

int main(void)
{
  const size_t size = 1000000;
  int array[size];

  for(size_t i=0; i<size; ++i) {
    array[i] = Rand() & 1;
  }

  struct timespec start, stop;

  clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &start);
  for(size_t i=0; i<size; ++i) {
    array[i] *= 123456;
    //if(array[i]) array[i] = 123456;
  }
  clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &stop);

  printf("size: %zu\t nsec: %09zu\n", size, diff(&start, &stop));
}

私の結果:

コンピューター:クアッドコアAMD Phenom @ 2.5GHz、Linux、GCC 4.7、コンパイル済み

$ gcc arr.c -std=gnu99 -lrt -O3 -march=native
  • ifバージョン:〜5-10ms
  • *=バージョン:〜1.3ms
47
Nicu Stiurca

あなたのような小さな配列の場合、別のアルゴリズムを見つけようとしても意味がなく、値が特定のパターンにない場合、単純なループがそれを行う唯一の方法です。

ただし、非常に大きな配列(数百万のエントリを話している)がある場合は、作業をスレッドに分割できます。個々のスレッドは、データセット全体の小さい部分を処理します。

15

これもベンチマークしたいかもしれません:

for(int i = 0; i < size ; i++){
  array[i] = (~(array[i]-1) & 123456);
}

SchighSchaghと同じベンチマークを実行しましたが、セットアップにほとんど違いはありません。ただし、ユーザーによって異なる場合があります。

編集:プレスを停止します!

「:」の間の引数が定数である場合、x86は三項演算子を「分岐解除」できることを思い出しました。次のコードを検討してください。

for(size_t i=0; i<size; ++i) {
    array[i] = array[i] ? 123456 : 0;
}

元のコードのように見えますか?さて、逆アセンブリは、ブランチなしでコンパイルされたことを示しています。

  for(size_t i=0; i<size; ++i) {
00E3104C  xor         eax,eax  
00E3104E  mov         edi,edi  
        array[i] = array[i] ? 123456 : 0;
00E31050  mov         edx,dword ptr [esi+eax*4]  
00E31053  neg         edx  
00E31055  sbb         edx,edx  
00E31057  and         edx,1E240h  
00E3105D  mov         dword ptr [esi+eax*4],edx  
00E31060  inc         eax  
00E31061  cmp         eax,5F5E100h  
00E31066  jb          wmain+50h (0E31050h)  
    }

パフォーマンスの点では、元のSchighSchaghソリューションと同等か、それより少し優れているようです。ただし、読みやすく柔軟です。たとえば、0と1以外の値を持つarray [i]を使用できます。

結論としては、ベンチマークと解体の概要をご覧ください。

13
gwiazdorrr

配列はキャッシュに収まるほど小さいので、SIMDを使用する価値があります:(テストされていません)

  mov ecx, size
  lea esi, [array + ecx * 4]
  neg ecx
  pxor xmm0, xmm0
  movdqa xmm1, [_vec4_123456]  ; value of { 123456, 123456, 123456, 123456 }
_replaceloop:
  movdqa xmm2, [esi + ecx * 4] ; assumes the array is 16 aligned, make that true
  add ecx, 4
  pcmpeqd xmm2, xmm0
  pandn xmm2, xmm1
  movdqa [esi + ecx * 4 - 16], xmm2
  jnz _replaceloop

2でアンロールすると役立つ場合があります。

SSE4.1を使用している場合、pmulldでSchighSchaghの乗算トリックを使用できます。

7
harold

さまざまなバージョンのアルゴリズムをプロファイルするためのWin32コードを次に示します(VS2010 Expressを使用してコンパイルし、デフォルトのリリースビルドを使用):

_#include <windows.h>
#include <stdlib.h>
#include <stdio.h>

const size_t
  size = 0x1D4C00;

_declspec(align(16)) int
  g_array [size];

_declspec(align(16)) int
  _vec4_123456 [] = { 123456, 123456, 123456, 123456 };

void Test (void (*fn) (size_t, int *), char *test)
{
  printf ("Executing test: %s\t", test);

  for(size_t i=0; i<size; ++i) {
    g_array[i] = Rand() & 1;
  }

  LARGE_INTEGER
    start,
    end;

  QueryPerformanceCounter (&start);

  fn (size, g_array);

  QueryPerformanceCounter (&end);

  printf("size: %u\t count: %09u\n", size, (int) (end.QuadPart - start.QuadPart));
}

void Test1 (size_t size, int *array)
{
  for(size_t i=0; i<size; ++i) {
    array[i] *= 123456;
  }
}

void Test2 (size_t size, int *array)
{
  for(size_t i=0; i<size; ++i) {
    if(array[i]) array[i] = 123456;
  }
}

void Test3 (size_t array_size, int *array)
{
  __asm
  {
    mov edi,array
    mov ecx, array_size 
    lea esi, [edi + ecx * 4]
    neg ecx
    pxor xmm0, xmm0
    movdqa xmm1, [_vec4_123456]  ; value of { 123456, 123456, 123456, 123456 }
_replaceloop:
    movdqa xmm2, [esi + ecx * 4] ; assumes the array is 16 aligned, make that true
    add ecx, 4
    pcmpeqd xmm2, xmm0
    pandn xmm2, xmm1
    movdqa [esi + ecx * 4 - 16], xmm2
    jnz _replaceloop
  }
}

void Test4 (size_t array_size, int *array)
{
  array_size = array_size * 8 / 12;

  __asm
  {
        mov edi,array
        mov ecx,array_size
        lea esi,[edi+ecx*4]
                                      lea edi,[edi+ecx*4]
        neg ecx
                                      mov edx,[_vec4_123456]
        pxor xmm0,xmm0
        movdqa xmm1,[_vec4_123456]
replaceloop:
        movdqa xmm2,[esi+ecx*4]
                                      mov eax,[edi]
                                      mov ebx,[edi+4]
        movdqa xmm3,[esi+ecx*4+16]
                                      add edi,16
        add ecx,9
                                      imul eax,edx    
        pcmpeqd xmm2,xmm0
                                      imul ebx,edx
        pcmpeqd xmm3,xmm0
                                      mov [edi-16],eax
                                      mov [edi-12],ebx
        pandn xmm2,xmm1
                                      mov eax,[edi-8]
                                      mov ebx,[edi-4]
        pandn xmm3,xmm1
                                      imul eax,edx    
        movdqa [esi+ecx*4-36],xmm2
                                      imul ebx,edx
        movdqa [esi+ecx*4-20],xmm3
                                      mov [edi-8],eax
                                      mov [edi-4],ebx
        loop replaceloop
  }
}

int main()
{
    Test (Test1, "Test1 - mul");
    Test (Test2, "Test2 - branch");
    Test (Test3, "Test3 - simd");
    Test (Test4, "Test4 - simdv2");
}
_

テスト用です:if()...を使用するC、乗算、ハロルドのsimdバージョンと私のsimdバージョンを使用するC。

何回も実行します(プロファイリングの際、複数の実行で結果を平均化する必要があることを思い出してください)。

これは、アルゴリズムが各メモリ項目に対してほとんど作業を行っていないため、それほど驚くことではありません。これが意味することは、実際の制限要因はCPUとメモリ間の帯域幅であり、CPUがデータのプリフェッチを支援している場合でも、CPUは常にメモリが追いつくのを待機していることです(ia32がデータを直線的に検出およびプリフェッチします)。

3
Skizz

別の配列または他のデータ構造を使用して、1に設定した要素のインデックスを追跡し、それらの要素のみにアクセスできます。これは、1つに設定されている要素が少数しかない場合に最適に機能します

2
Sven

これはより速く証明されるかもしれません。

for(int i = 0; i < size ; i++){
  array[i] = ((123456 << array[i]) - 123456);
}

編集:ビット単位の操作を左シフトに変更しました。

2
Abhinav

配列の割り当てを高速化するもう1つの方法として、cインラインアセンブリを使用できます。以下のように、

#include<stdio.h>
#include<string.h>
#include<stdlib.h>

const int size = 100000; 
void main(void) {
  int array[size];
  int value = 1000;

  __asm__ __volatile__("cld\n\t"
          "rep\n\t"
          "stosl\n\t"
          :
          :"c"(size*4), "a"(value), "D"(array)
          :
         );

  printf("Array[0] : %d \n", array[0]);
}

これは、配列値を割り当てるために単純なcプログラムと比較したときの速度です。また、stosl命令には4クロックサイクルかかります。

0
Mohanraj