CUDAで遊んでみた

nVIDIAGPUで並列計算を行うプログラムをCで書けるっていうCUDAで遊んでみました。
ということで、みしょさんの11以下の素因数を持つ携帯電話番号には価値がない。でのお題をやってみました。
いくつかあるのですが、このお題。

僕の携帯電話番号は113未満の素因数を持ちません。これはどの程度すばらしい電話番号なのでしょう?つまり,80から始まる10桁の自然数について,113未満の素因数を持たないものはどれくらいあるでしょうか?


と思ったら、なんだかCUDAでは大きい数字がうまく扱えない様子。
なので、10から始まる9桁の自然数について、113未満の素因数を持たないものを数えてみることにしました。
実行結果は次のとおり
ホストが、CPUでやった結果です。
Kernel1が共有メモリを使わずにGPUでやった場合。Kernel2が共有メモリ使った場合。

100000000から109999999までで113より小さい素因数を持たないものの数
ホスト 計算時間=1326.090088(ms) 結果 1157751
Kernel1 計算時間=6846.341309(ms) 計算結果=1157751
Kernel2 計算時間=5039.050293(ms) 計算結果=1157751


ぐぬぬGPU使ったほうが遅くなってます。共有メモリ使うとかなりスピードが改善してることもわかりますね。
ということで、整数演算だと、CPUよりも遅いかもしれない。
使ったのがGeForce8400GSという、最廉価のバージョンなので、8800GTとかを使うとCPUよりも速くなるのかもしれません。
ちなみに、Javaだと1556msになってます。Cよりちょっと遅いですが、数回繰り返すようにして最適化がかかればCと同じくらいの速さになりそうですね。


一応、80で始まる10桁を求めると、11579542になりました。Javaだと31秒、Cだと15秒程度でした。64ビット整数の演算は、Javaは苦手なんでしょか?


ということでソース。
Java

import java.util.*;

public class CountPrime {
    public static void main(String[] args){
        //素数を求める
        boolean[] primeFlag = new boolean[113];
        for(int i = 0; i < primeFlag.length; ++i) primeFlag[i] = true;
        primeFlag[1] = false;
        for(int i = 1; i < primeFlag.length; ++i){
            if(!primeFlag[i]) continue;
            for(int j = i * 2; j < primeFlag.length; j += i){
                primeFlag[j] = false;
            }
        }
        //素数の配列を作成
        List<Integer> primsList = new ArrayList<Integer>();
        for(int i = 1; i < primeFlag.length; ++i){
            if(primeFlag[i]) primsList.add(i);
        }
        int[] prims = new int[primsList.size()];//Listのままだと65秒 配列だと31秒
        for(int i = 0; i < primsList.size(); ++i){
            prims[i] = primsList.get(i);
        }
        
        //
        long start = System.nanoTime();
        int count = 0;
        LOOP:
        //for(long lg = 8000000000L; lg < 8100000000L; ++lg){
        for(int lg = 100000000; lg < 110000000; ++lg){
            for(int i : prims){
                if(lg % i == 0) continue LOOP;
            }
            count++;
        }
        System.out.println((System.nanoTime() - start) / 1000 / 1000);
        System.out.println(count);
        // 090の場合 11579520
        // 080の場合 11579542
        // 070の場合 11579562
    }
}


で、CUDA版

#include <stdio.h>
#include <conio.h>
#include <cutil.h>


#define BLOCK 32
#define START 100000000L
#define RANGE  10000000L
#define MOD(X,Y) (X)%(Y)
#define PRIM 113
#define LONG unsigned __int32

//プロトタイプ
void Host(long *a, int count);
__global__ void Kernel1(LONG *a, LONG *b, int count);
__global__ void Kernel2(LONG *a, LONG *b, int count);

void SetTimer(unsigned int *t);
float EndTimer(unsigned int *t);


int main()
{
	printf("%ldから%ldまでで%dより小さい素因数を持たないものの数\n", START, START + RANGE - 1, PRIM);
	//素数を求める
	bool primflag[PRIM];
	for(int i = 1; i < PRIM; ++i){
		primflag[i] = true;
	}
	primflag[1] = false;
	for(int i = 1; i < PRIM; ++i){
		if(!primflag[i]) continue;
		for(int j = i * 2; j < PRIM; j += i){
			primflag[j] = false;
		}
	}
	//配列に格納
	int count = 0;
	LONG prims[PRIM];
	for(int i = 1; i < PRIM; ++i){
		if(!primflag[i]) continue;
		prims[count++] = i;
	}
	
	unsigned int timer;
	CUT_DEVICE_INIT();
	LONG pcount = 0;
	//ホスト側で計算
	
	SetTimer(&timer);

	pcount = 0;
	for(LONG lg = START; lg < START + RANGE; ++lg){
		bool flag = true;
		for(int i = 0; i < count; ++i){
			if(MOD(lg, prims[i]) == 0){
				flag = false;
				break;
			}
		}
		if(flag) pcount++;
	}


	printf("ホスト 計算時間=%f(ms)", EndTimer(&timer));
	printf(" 結果 %d\n", pcount);
	

	//初期化
	LONG *d_a;
	LONG *d_b;
	LONG ret[PRIM * BLOCK];
	dim3 grid(BLOCK, 1, 1);
	dim3 threads(count, 1, 1);

	//=== GPU ===
	for(int k = 0; k < 2; ++k){
	SetTimer(&timer);

	//CUDAグローバルメモリの確保
	cudaMalloc((void**) &d_a, sizeof(LONG) * count);
	cudaMalloc((void**) &d_b, sizeof(LONG) * count * BLOCK);
	cudaMemset(d_b, 0, sizeof(LONG) * count * BLOCK);

	//CUDAへコピー
	cudaMemcpy(d_a, prims, sizeof(LONG) * count,
		cudaMemcpyHostToDevice);

	//実行
	switch(k){
		case 0:
			Kernel1 <<< grid, threads >>>(d_a, d_b, count);
			break;
		case 1:
			Kernel2 <<< grid, threads >>>(d_a, d_b, count);
			break;
	}

	//CUDAからコピー
	cudaMemcpy(ret, d_b,
		sizeof(LONG) * count * BLOCK, cudaMemcpyDeviceToHost);
	//集計
	pcount = 0;
	for(int i = 0; i < count * BLOCK; ++i){
		pcount += ret[i];
	}

	printf("Kernel%d 計算時間=%f(ms)", k + 1, EndTimer(&timer));
	printf(" 計算結果=%d\n", pcount);

	//メモリ解放
	cudaFree(d_a);
	cudaFree(d_b);
	}

	_getch();
}

__global__ void Kernel1(LONG *a, LONG *b, int count)
{
	int idx = blockIdx.x * count + threadIdx.x;
	LONG split = RANGE / count / BLOCK;
	LONG s = START + idx * split;
	//LONG e = START + (idx + 1) * split;
	LONG e = (idx + 1 == BLOCK * count) ? START + RANGE : START + split * (idx + 1);
	//printf("%d:%d %lld - %lld\n", blockIdx.x, threadIdx.x, s, e);
	LONG pcount = 0;
	for(LONG lg = s; lg < e; ++lg){
		bool flag = true;
		for(int i = 0; i < count; ++i){
			if(MOD(lg, a[i]) == 0){
				flag = false;
				break;
			}
		}
		if(flag) pcount++;
	}
	b[idx] = pcount;

}

__global__ void Kernel2(LONG *a, LONG *b, int count)
{
	int idx = blockIdx.x * count + threadIdx.x;
	LONG split = RANGE / count / BLOCK;
	LONG s = START + idx * split;
	LONG e = (idx + 1 == BLOCK * count) ? START + RANGE : START + split * (idx + 1);
	//printf("%d:%d %lld - %lld\n", blockIdx.x, threadIdx.x, s, e);

	__shared__ LONG prim[PRIM];
	prim[threadIdx.x] = a[threadIdx.x];
	__syncthreads();

	LONG pcount = 0;
	for(LONG lg = s; lg < e; ++lg){
		bool flag = true;
		for(int i = 0; i < count; ++i){
			if(MOD(lg, prim[i]) == 0){
				flag = false;
				break;
			}
		}
		if(flag) pcount++;
	}
	b[idx] = pcount;

}


void SetTimer(unsigned int *t){
	cutCreateTimer(t);
	cutStartTimer(*t);
}
float EndTimer(unsigned int *t){
	cutStopTimer(*t);
	float tmp = cutGetTimerValue(*t);
	cutDeleteTimer(*t);
	return tmp;
}