GPUってよく聞くけど、何者なの? 実例とともに解説します!
2025/03/09 GPU HTML Javascript
GPUってよく聞くけど、何者なの?
GPUとは“Graphics Processing Unit”の略で……などと専門的な話をしても、パソコンが好きな人以外はピンと来ないでしょう。そこでこんな例え話を持ってきました。
例え話
あなたは良い商品を思いつき、これを作りたいと思いました。しかし自力では作れなさそうなので、製造業者に外注する事になりました。
-
A会社
- 従業員8人の会社です。
- 8人で分担し、手作業で丁寧に仕事を行います。
- 内容の急な変更もOKです。変更があればいつでも遠慮なくご連絡ください!
-
B会社
- 1000台の機械を使って、速やかに作業を終わらせます。
- 機械のセットアップにかなりの時間を頂くことになります。
- 急な仕事内容の変更は出来ません。ご注意ください。
※違う作業を行う場合、機械を再セットアップする時間がかかります。
どちらの会社も一長一短ですね。どちらの会社を利用しましょうか?
例えば「商品を10個作りたい」程度の依頼なら、A会社に注文するのが良いでしょう。1000台の機械を準備している間に、8人の人間で作業をした方が早く終わります。しかも内容をいつでも変更できるという強みもあります。
一方で「大量生産だ! 1億個作るぞ!」という依頼ならB会社に注文するのが良いでしょう。機械のセットアップに多少時間がかかったとしても、一度セットアップが完了したら一瞬で1億個の商品が出来上がります。ただし、途中で変更は出来ないので、注意が必要です。
解説
A会社はCPU、つまり普通の計算機をイメージしています。大量の並列演算は行えない代わりに、キーボード操作やマウス操作に応じてすぐに処理内容を変更する事が出来ます。
そしてB会社がGPU、つまり高速な並列演算を行う機会です。大量の並列演算を行えますが、処理内容は途中で変更できません。
そう言われてもピンとこないぞ?
百聞は一見にしかず
下手な例え話よりも、実例を見た方が分かりやすいですよね! という訳で今度は実例をお見せしようと思います。
マンデルブロ集合
今から「マンデルブロ集合」という図形を描画するプログラムをCPUとGPUの両方で書いてみます。この図形は複素数平面上に描画される物なのですが、いったん複素数を使わずに説明しますね。
以下のように二つの数列xn、ynを定義します。この数列はRとIという二つの値によって挙動が変化します。
x0=0,y0=0
xn+1=xn2−yn2+R
yn+1=2xnyn+I
この二つの数列のどちらかが n→∞ の極限で無限大に発散するかどうかを調べる作業を行います。
ちなみに、この計算を複素数を使って表すと次のようになります。たったの二行になってしまうのが、複素数の美しさですよね! (なお、c=R+Iiとします)
z0=0
zn+1=zn2+c
マンデルブロ集合をCPUで描く
n→∞の値を正しく計算することは不可能なので、ここではn=2400の時にx2+y2=∣z∣が2以上になるかどうかを確認する事にします。
n=2400でも2より小さいときは黒色、そうでないときは初めて絶対値が2を超えた時のnの値に応じて着色することにします。
まず、数列を計算する関数getExceedN
を作りました。マンデルブロ集合内(最後まで小さな値だった)ならば-1を返し、そうでないなら初めて絶対値が2以上になった時のnの値を返します。なお、言語はJavaScriptです。
function getExceedN(r, i){
let x = 0;
let y = 0;
for(let n=1;n<=2400; ++n){
let tmp = x;
x = tmp * tmp - y * y + r;
y = 2 * tmp * y + i;
if (x * x + y * y >= 4) return n;
}
return -1;
}
次にHSV色空間をRGB色空間に変換する関数hsv2rgb
を書きました。虹のグラデーションを生成する為のコードなのですが、詳しくは割愛します。
// h(0 to 360), s(0 to 1), v(0 to 1)
// rgb(0 to 255)
function hsv2rgb(h, s, v){
let max_value = v * 255;
let min_value = max_value * (1 - s);
let value = (1.0 - Math.abs((h % 120) - 60) / 60);
value *= (max_value - min_value);
value += min_value;
switch(Math.floor(h / 60)){
case 0:
return [max_value, value, min_value];
case 1:
return [value, max_value, min_value];
case 2:
return [min_value, max_value, value];
case 3:
return [min_value, value, max_value];
case 4:
return [value, min_value, max_value];
case 5:
return [max_value, min_value, value];
}
return [0, 0, 0];
}
これをキャンバスに描画するコードを書きます。なお、キャンバスサイズは800x800に固定しています。
また、表示領域はX軸(実軸)が-2から0.5、Y軸(虚軸)が-1.25から1.25です。
function renderWithCpu(dst, cx, cy, scale){
for (let y = 0; y < 800; ++y) {
let i = y * 800 * 4;
for (let x = 0; x < 800; ++x) {
let data = [0, 0, 0];
let coord_x = ((x / 400) - 1.0) * scale + cx;
let coord_y = ((y / 400) - 1.0) * scale + cy;
let n = getExceedN(coord_x, coord_y);
if(n >= 0) data = hsv2rgb(240 - n / 10, 1.0, 1.0);
dst.data[i] = Math.floor(data[0]);
dst.data[i + 1] = Math.floor(data[1]);
dst.data[i + 2] = Math.floor(data[2]);
dst.data[i + 3] = 255;
i+=4;
}
}
}
function startCpu(){
let canvas = document.getElementById("canvas-cpu");
let ctx = canvas.getContext("2d");
let dst = ctx.createImageData(800, 800);
let start = performance.now();
renderWithCpu(dst, -0.75, 0, 1.25);
let end = performance.now();
ctx.putImageData(dst, 0, 0);
let text = "Calculation takes ";
text += Math.floor(end - start);
text += "ms.";
document.getElementById("cpu-log").innerText = text;
}
以下で実際に実行する事が出来ます。
Press the button above to start the calculation.
マンデルブロ集合をGPUで描く
まずは実際に実行できる物を載せます。CPUと比べて遥かに計算が速い事が見て取れます。
Press the button above to start the calculation.
同じ計算を行うプログラムをGPUで組んでいきます。
GPUを動かすプログラムを書く
GPUはC言語風のプログラム言語である「GLSL」を書く必要があります。以下がそのコードです。
Vertex Shader
#version 300 es
layout (location = 0) in vec2 position;
void main(void){
gl_Position = vec4(position, 0.0, 1.0);
}
Fragment Shader
#version 300 es
precision mediump float;
uniform float scale;
uniform vec2 pos;
out vec4 out_color;
int get_exceed_n(vec2 coord){
vec2 xy;
for(int n=1;n<=2400; ++n){
float tmp = xy.x;
xy.x = tmp * tmp - xy.y * xy.y + coord.x;
xy.y = 2.0 * tmp * xy.y + coord.y;
if (xy.x * xy.x + xy.y * xy.y >= 4.0) return n;
}
return -1;
}
// h:(0 to 360), s:(0 to 1), v:(0 to 1)
// rgb:vec3(0 to 1)
vec3 hsv2rgb(float h, float s, float v){
float max_value = v;
float min_value = max_value * (1.0 - s);
float value = (1.0 - abs(mod(h, 120.0) - 60.0) / 60.0);
value *= (max_value - min_value);
value += min_value;
if(h < 60.0){
return vec3(max_value, value, min_value);
}
else if(h < 120.0){
return vec3(value, max_value, min_value);
}
else if(h < 180.0){
return vec3(min_value, max_value, value);
}
else if(h < 240.0){
return vec3(min_value, value, max_value);
}
else if(h < 300.0){
return vec3(value, min_value, max_value);
}
else if(h < 360.0){
return vec3(max_value, min_value, value);
}
return vec3(0.0);
}
void main(void){
vec2 coord = (gl_FragCoord.xy / vec2(400.0, 400.0) - 1.0) * scale + pos;
int n = get_exceed_n(coord);
if(n < 0) out_color = vec4(0.0, 0.0, 0.0, 1.0);
else out_color = vec4(hsv2rgb(240.0 - float(n) * 0.1, 1.0, 1.0), 1.0);
}
む、難しい……。GLSLの事前知識が無いと、何をやっているのか訳が分からないですよね。
GPU計算において重要な部分だけかいつまんで説明すると、この部分をご覧ください。
void main(void){
vec2 coord = (gl_FragCoord.xy / vec2(400.0, 400.0) - 1.0) * scale + pos;
int n = get_exceed_n(coord);
if(n < 0) out_color = vec4(0.0, 0.0, 0.0, 1.0);
else out_color = vec4(hsv2rgb(240.0 - float(n) * 0.1, 1.0, 1.0), 1.0);
}
ざっくり説明すると、裏では以下のようなコードが動いています。
for (let y = 0; y < 800; ++y) {
for (let x = 0; x < 800; ++x) {
gl_FragCoord.xy = [x, y];
main();
結果画像[y][x]=out_color;
}
}
800x800個の座標に対して、main
関数が動きます。そしてout_color
に保存された色を結果画像に書き込んでいます。
これだけならCPUと同じですが、実はGPUではこのmain
関数が800x800個のスレッドで並列計算されているのです。1つずつ順番に計算するCPUに比べて、単純計算で800x800倍の処理速度です!(実際にはもう少し遅いですが)
上記のコードを以下のようにhtml内に埋め込みます。
<script id="vertex-shader" type="vertex-shader">
ここにVertex Shaderを書く
</script>
<script id="fragment-shader" type="fragment-shader">
ここにFragment Shaderを書く
</script>
プログラムをGPUに書き込む
続いてこのプログラムをGPUに書き込みましょう。そのJavaScriptコードはこのようになります。
let canvas = document.getElementById("canvas-gpu");
let gl = canvas.getContext("webgl2");
let vshader_source = document.getElementById("vertex-shader").text.trim();
let fshader_source = document.getElementById("fragment-shader").text.trim();
let vshader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vshader, vshader_source);
gl.compileShader(vshader);
if(!gl.getShaderParameter(vshader, gl.COMPILE_STATUS)){
console.log(gl.getShaderInfoLog(vshader));
return;
}
let fshader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fshader, fshader_source);
gl.compileShader(fshader);
if(!gl.getShaderParameter(fshader, gl.COMPILE_STATUS)){
console.log(gl.getShaderInfoLog(fshader));
return;
}
let program = gl.createProgram();
gl.attachShader(program, vshader);
gl.attachShader(program, fshader);
gl.linkProgram(program);
gl.deleteShader(vshader);
gl.deleteShader(fshader);
if(!gl.getProgramParameter(program, gl.LINK_STATUS)){
console.log(gl.getProgramInfoLog(program));
return;
}
gl.useProgram(program);
GPUメモリにデータを書き込む
四角形の四頂点の座標をGPUに書き込みます。
let vbo = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1,-1,-1,1,1,1,1,-1]), gl.STATIC_DRAW);
描画を行う
最後に描画するコードを書きます。
gl.enableVertexAttribArray(0);
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
gl.uniform1f(GpuCalculation.scale_loc, 1.25);
gl.uniform2f(GpuCalculation.pos_loc, -0.75, 0.0);
gl.drawArrays(gl.TRIANGLE_FAN, 0, 4);
gl.flush();
よく分からないコードをつらつらと書きましたが、取り敢えず「GPUはセットアップが大変」という事だけ知って頂ければと思います。
最後にサンプルコードを載せておきます。
what_is_gpu_cpu.js
function getExceedN(r, i){
let x = 0;
let y = 0;
for(let n=1;n<=2400; ++n){
let tmp = x;
x = tmp * tmp - y * y + r;
y = 2 * tmp * y + i;
if (x * x + y * y >= 4) return n;
}
return -1;
}
// h(0 to 360), s(0 to 1), v(0 to 1)
// rgb(0 to 255)
function hsv2rgb(h, s, v){
let max_value = v * 255;
let min_value = max_value * (1 - s);
let value = (1.0 - Math.abs((h % 120) - 60) / 60);
value *= (max_value - min_value);
value += min_value;
switch(Math.floor(h / 60)){
case 0:
return [max_value, value, min_value];
case 1:
return [value, max_value, min_value];
case 2:
return [min_value, max_value, value];
case 3:
return [min_value, value, max_value];
case 4:
return [value, min_value, max_value];
case 5:
return [max_value, min_value, value];
}
return [0, 0, 0];
}
function renderWithCpu(dst, cx, cy, scale){
for (let y = 0; y < 800; ++y) {
let i = y * 800 * 4;
for (let x = 0; x < 800; ++x) {
let data = [0, 0, 0];
let coord_x = ((x / 400) - 1.0) * scale + cx;
let coord_y = ((y / 400) - 1.0) * scale + cy;
let n = getExceedN(coord_x, coord_y);
if(n >= 0) data = hsv2rgb(240 - n / 10, 1.0, 1.0);
dst.data[i] = Math.floor(data[0]);
dst.data[i + 1] = Math.floor(data[1]);
dst.data[i + 2] = Math.floor(data[2]);
dst.data[i + 3] = 255;
i+=4;
}
}
}
function startCpu(){
let canvas = document.getElementById("canvas-cpu");
let ctx = canvas.getContext("2d");
let dst = ctx.createImageData(800, 800);
let start = performance.now();
renderWithCpu(dst, -0.75, 0, 1.25);
let end = performance.now();
ctx.putImageData(dst, 0, 0);
let text = "Calculation takes ";
text += Math.floor(end - start);
text += "ms.";
document.getElementById("cpu-log").innerText = text;
let button = document.getElementById("btn-cpustart");
button.disabled = false;
}
function clickStartCpu(){
let button = document.getElementById("btn-cpustart");
button.disabled = true;
setTimeout(startCpu, 0);
}
what_is_gpu_gpu.js
let GpuCalculation = {
gl:null,
program:null,
scale_loc:null,
pos_loc:null,
vbo:null,
ext:null,
setup_time:null,
query:null
};
function startGpu(){
let canvas = document.getElementById("canvas-gpu");
let gl = canvas.getContext("webgl2");
if(!gl){
document.getElementById("gpu-log").innerText = "GPU not Supported";
return;
}
let vshader_source = document.getElementById("vertex-shader").text.trim();
let fshader_source = document.getElementById("fragment-shader").text.trim();
let setup_start = performance.now();
// Shader initialization
let vshader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vshader, vshader_source);
gl.compileShader(vshader);
if(!gl.getShaderParameter(vshader, gl.COMPILE_STATUS)){
console.log(gl.getShaderInfoLog(vshader));
document.getElementById("gpu-log").innerText = "GPU not Supported";
return;
}
let fshader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fshader, fshader_source);
gl.compileShader(fshader);
if(!gl.getShaderParameter(fshader, gl.COMPILE_STATUS)){
console.log(gl.getShaderInfoLog(fshader));
document.getElementById("gpu-log").innerText = "GPU not Supported";
return;
}
let program = gl.createProgram();
gl.attachShader(program, vshader);
gl.attachShader(program, fshader);
gl.linkProgram(program);
gl.deleteShader(vshader);
gl.deleteShader(fshader);
if(!gl.getProgramParameter(program, gl.LINK_STATUS)){
console.log(gl.getProgramInfoLog(program));
document.getElementById("gpu-log").innerText = "GPU not Supported";
return;
}
gl.useProgram(program);
let scale_loc = gl.getUniformLocation(program, 'scale');
let pos_loc = gl.getUniformLocation(program, 'pos');
// Buffer initialization
let vbo = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1,-1,-1,1,1,1,1,-1]), gl.STATIC_DRAW);
gl.finish();
let setup_end = performance.now();
let ext = gl.getExtension("EXT_disjoint_timer_query_webgl2");
GpuCalculation.gl = gl;
GpuCalculation.program = program;
GpuCalculation.scale_loc = scale_loc;
GpuCalculation.pos_loc = pos_loc;
GpuCalculation.vbo = vbo;
GpuCalculation.setup_time = setup_end - setup_start;
GpuCalculation.ext = ext;
GpuCalculation.query = null;
if(!ext || !("TIME_ELAPSED_EXT" in ext) || !("GPU_DISJOINT_EXT" in ext)){
renderGpu();
let text = "Setup takes ";
text += Math.floor(GpuCalculation.setup_time);
text += "ms. Calculation time is not available.";
document.getElementById("gpu-log").innerText = text;
let button = document.getElementById("btn-cpustart");
button.disabled = false;
return;
}
requestAnimationFrame(renderLoop);
}
function renderGpu() {
let gl = GpuCalculation.gl;
gl.enableVertexAttribArray(0);
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
gl.uniform1f(GpuCalculation.scale_loc, 1.25);
gl.uniform2f(GpuCalculation.pos_loc, -0.75, 0.0);
gl.drawArrays(gl.TRIANGLE_FAN, 0, 4);
gl.flush();
}
function renderLoop(){
let gl = GpuCalculation.gl;
if(!GpuCalculation.query){
GpuCalculation.query = gl.createQuery();
gl.beginQuery(GpuCalculation.ext.TIME_ELAPSED_EXT, GpuCalculation.query);
renderGpu();
gl.endQuery(GpuCalculation.ext.TIME_ELAPSED_EXT);
}
let disjoint = gl.getParameter(GpuCalculation.ext.GPU_DISJOINT_EXT);
if(disjoint){
gl.deleteQuery(GpuCalculation.query);
GpuCalculation.query = null;
}
else {
let available = gl.getQueryParameter(GpuCalculation.query, gl.QUERY_RESULT_AVAILABLE);
if(available){
let result = gl.getQueryParameter(GpuCalculation.query, gl.QUERY_RESULT);
let text = "Setup takes ";
text += Math.floor(GpuCalculation.setup_time);
text += "ms. Calculation time is ";
text += Math.floor(result / (1000 * 1000));
text += "ms.";
document.getElementById("gpu-log").innerText = text;
let button = document.getElementById("btn-cpustart");
button.disabled = false;
gl.deleteQuery(GpuCalculation.query);
GpuCalculation.query = null;
return;
}
}
requestAnimationFrame(renderLoop);
}
function clickStartGpu(){
let button = document.getElementById("btn-gpustart");
button.disabled = true;
setTimeout(startGpu, 0);
}
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<p><button id="btn-cpustart" onclick="clickStartCpu()">Start</button></p>
<p id="cpu-log">Press the button above to start the calculation.</p>
<canvas width="800" height="800" id="canvas-cpu"></canvas>
<p><button id="btn-gpustart" onclick="clickStartGpu()">Start</button></p>
<p id="gpu-log"></p>
<canvas width="800" height="800" id="canvas-gpu"></canvas>
<script src="what_is_gpu_cpu.js"></script>
<script id="vertex-shader" type="vertex-shader">
#version 300 es
layout (location = 0) in vec2 position;
void main(void){
gl_Position = vec4(position, 0.0, 1.0);
}
</script>
<script id="fragment-shader" type="fragment-shader">
#version 300 es
precision mediump float;
uniform float scale;
uniform vec2 pos;
out vec4 out_color;
int get_exceed_n(vec2 coord){
vec2 xy;
for(int n=1;n<=2400; ++n){
float tmp = xy.x;
xy.x = tmp * tmp - xy.y * xy.y + coord.x;
xy.y = 2.0 * tmp * xy.y + coord.y;
if (xy.x * xy.x + xy.y * xy.y >= 4.0) return n;
}
return -1;
}
// h:(0 to 360), s:(0 to 1), v:(0 to 1)
// rgb:vec3(0 to 1)
vec3 hsv2rgb(float h, float s, float v){
float max_value = v;
float min_value = max_value * (1.0 - s);
float value = (1.0 - abs(mod(h, 120.0) - 60.0) / 60.0);
value *= (max_value - min_value);
value += min_value;
if(h < 60.0){
return vec3(max_value, value, min_value);
}
else if(h < 120.0){
return vec3(value, max_value, min_value);
}
else if(h < 180.0){
return vec3(min_value, max_value, value);
}
else if(h < 240.0){
return vec3(min_value, value, max_value);
}
else if(h < 300.0){
return vec3(value, min_value, max_value);
}
else if(h < 360.0){
return vec3(max_value, min_value, value);
}
return vec3(0.0);
}
void main(void){
vec2 coord = (gl_FragCoord.xy / vec2(400.0, 400.0) - 1.0) * scale + pos;
int n = get_exceed_n(coord);
if(n < 0) out_color = vec4(0.0, 0.0, 0.0, 1.0);
else out_color = vec4(hsv2rgb(240.0 - float(n) * 0.1, 1.0, 1.0), 1.0);
}
</script>
<script src="what_is_gpu_gpu.js"></script>
</body>
</html>