次のような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の策略によってこれを行うためのより速い方法がありますか、またはこれはプロセッサにとって最適なコードですか?
最初に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あなたのような小さな配列の場合、別のアルゴリズムを見つけようとしても意味がなく、値が特定のパターンにない場合、単純なループがそれを行う唯一の方法です。
ただし、非常に大きな配列(数百万のエントリを話している)がある場合は、作業をスレッドに分割できます。個々のスレッドは、データセット全体の小さい部分を処理します。
これもベンチマークしたいかもしれません:
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]を使用できます。
結論としては、ベンチマークと解体の概要をご覧ください。
配列はキャッシュに収まるほど小さいので、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の乗算トリックを使用できます。
さまざまなバージョンのアルゴリズムをプロファイルするための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がデータを直線的に検出およびプリフェッチします)。
別の配列または他のデータ構造を使用して、1に設定した要素のインデックスを追跡し、それらの要素のみにアクセスできます。これは、1つに設定されている要素が少数しかない場合に最適に機能します
これはより速く証明されるかもしれません。
for(int i = 0; i < size ; i++){
array[i] = ((123456 << array[i]) - 123456);
}
編集:ビット単位の操作を左シフトに変更しました。
配列の割り当てを高速化するもう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クロックサイクルかかります。