こんにちは!私は普段あまり開発という開発はしないのですが、時折Visual Studioなどを使用して、デモ環境を作ったりします。普段開発をガリガリやられている方からすると、「デバッグビルド」や「リリースビルド」、「x86(32 bit)」と「x64(64 bit)」の違いなどは当たり前の知識なのかもしれませんが、私には正直あまり違いがわかりません。”とりあえず使える”というレベルです。

そこで今回は、普段なんとなくで切り替えているそれぞれの設定の違いを探るべく、Visual Studio × C言語のベンチマークテストを実施してみました!私と同じようによくわからんけど使っているという方はもちろん、プログラミングや開発に興味がある方はぜひ最後までお付き合いください。

 

そもそもの各設定の違い

 

アーキテクチャ:x86とx64

プログラムは、CPUという「道路」の上を走る「車」のようなものです。この道路の広さが、32 bitや64 bitという言葉で表現されます。これは、一度に処理できるデータ量(情報の幅)を指します。

 

x86(32 bit)

x86は、32 bitアーキテクチャのCPUを指し、一度に32 bit(4 byte)のデータを扱います。これは「片側1車線の道路」に例えられます。この幅では、扱えるメモリ空間の大きさに限界があり、約4 GBまでしか直接アクセスできません。多くのパソコンがまだ4 GB以下のメモリを搭載していた時代には、このx86が主流でした。

 

x64(64 bit)

一方、x64は64 bitアーキテクチャのCPUを指し、一度に64 bit(8 byte)のデータを扱います。これは「片側2車線の広い道路」に例えられます。64 bitのCPUは、理論上膨大な量のメモリ(約16エクサバイト)を扱うことができ、4 GBの壁を超えて大量のメモリを搭載したパソコンの性能を最大限に引き出すことができます。現在、我々が使っているほとんどのパソコンはx64アーキテクチャです。

まとめると…

  • x86は、32 bit CPU向け。メモリは最大4 GB程度までしか扱えません。
  • x64は、64 bit CPU向け。4 GB以上の大容量メモリを扱うことができ、高速なデータ処理が可能です。
 

ビルド構成:DebugとRelease

次に、DebugとReleaseの違いです。これは、プログラムを「どのようにコンパイル(機械が理解できる言葉に翻訳)するか」という設定です。

 

Debug(デバッグ)

Debugモードは、プログラムのバグ(不具合)を見つけるための設定です。コンパイラは、以下のような特別な処理を行います。

  • デバッグ情報の追加

    ソースコードの行番号や変数の中身など、デバッグに必要な情報を実行ファイルに含めます。これにより、開発ツール(デバッガ)を使ってプログラムの動きを細かく追跡できます。

  • 最適化の無効化

    コンパイラは、コードの実行速度を上げるための「最適化」処理をほとんど行いません。これは、最適化によってソースコードと実際の実行順序がずれてしまうと、デバッグが困難になるためです。

 

Release(リリース)

Releaseモードは、ユーザーに配布する「製品版」のプログラムを作るための設定です。コンパイラは、実行ファイルのパフォーマンスを最大化するために、以下のような処理を行います。

  • デバッグ情報の削除

    余計なデバッグ情報をすべて取り除きます。これにより、ファイルサイズが小さくなります。

  • 最適化の有効化

    プログラムの実行速度を上げるための最適化処理を最大限に行います。これにより、メモリの使用効率が上がり、プログラムがより高速に動作します。

 

いつ、どう使い分ける?

  • 開発中は「Debug」

    プログラムを作成し、テストしている間は、Debugモードでビルドします。不具合が発生した際に、Visual Studioの強力なデバッガ機能を使って、原因を特定できます。

  • 完成したら「Release」

    テストが完了し、安定したプログラムをユーザーに配布したり、パフォーマンスを測定したりする際は、Releaseモードでビルドします。このモードで作成された実行ファイルは、サイズが小さく、高速に動作します。

 

測定環境と方法

今回の検証に使用したノートPCのスペックはこちらです。(5年モノ)

  • プロセッサ:AMD Ryzen 7 4700U with Radeon Graphics 2.00 GHz
  • 実装 RAM:16.0 GB
  • ストレージ:477 GB SSD
  • システム:64 bit

そして、このPCで実行したのが、ひたすら整数の足し算やビット演算といったシンプルな計算を、任意の時間(単位:分)にわたって繰り返すプログラムです。これにより、CPUがどれだけの計算を処理できるか(MOPS: Million Operations Per Second)を測定します。バックグラウンドで動いているアプリケーションの負荷状況により多少変化はあるかもしれませんが、おおよそ同じ条件下で実行しています。今回は3分間計測します。
なお、このベンチマークはVisual Studio 2019を使用してコンパイル・実行しています。ファイル実行時に、任意の数字を渡すことで、指定した時間(単位:分)のスコアを計測します。

// int_bench_windows.c
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>

typedef struct {
    long long total_ops;
    long long sum;
    double elapsed_sec;
    double avg_mops;
    int completed;
} BenchResult;

BenchResult g_bench;

double get_time_sec(void) {
    static LARGE_INTEGER freq;
    LARGE_INTEGER now;
    if (freq.QuadPart == 0) {
        QueryPerformanceFrequency(&freq);
    }
    QueryPerformanceCounter(&now);
    return (double)now.QuadPart / freq.QuadPart;
}

void report_progress(double sec_total) {
    g_bench.elapsed_sec = sec_total;
    g_bench.avg_mops = g_bench.total_ops / sec_total / 1e6;
    printf("[%.2f sec] Total ops=%lld, Avg speed=%.2f MOPS\n",
        g_bench.elapsed_sec, g_bench.total_ops, g_bench.avg_mops);
}

void report_final(double sec_total) {
    g_bench.elapsed_sec = sec_total;
    g_bench.avg_mops = g_bench.total_ops / sec_total / 1e6;
    g_bench.completed = 1;
    printf("=== Benchmark Complete ===\n");
    printf("Total ops=%lld, time=%.2f sec, Avg speed=%.2f MOPS\n",
        g_bench.total_ops, g_bench.elapsed_sec, g_bench.avg_mops);
    printf("Final sum=%lld\n", g_bench.sum);
}

int main(int argc, char* argv[]) {
    int minutes = (argc >= 2) ? atoi(argv[1]) : 1;
    if (minutes <= 0) minutes = 1;
    int duration = minutes * 60;

    g_bench.total_ops = 0;
    g_bench.sum = 0;
    g_bench.completed = 0;

    double start = get_time_sec();
    double last_report = start;

    printf("=== Integer Benchmark for %d minutes ===\n", minutes);

    while (1) {
        for (int i = 0; i < 10000; i++) {
            g_bench.sum += (i * 13) ^ (i >> 3);
        }
        g_bench.total_ops += 10000;

        double now = get_time_sec();
        double sec_total = now - start;
        double sec_since_last = now - last_report;

        if (sec_since_last >= 60.0) {
            report_progress(sec_total);
            last_report = now;
        }
        if (sec_total >= duration) {
            report_final(sec_total);
            break;
        }
    }
    return 0;
}
> benchmark.exe 3    // 3分のスコア計測が実行されます
 

ベンチマーク結果

気になるベンチマーク結果を見ていきましょう。
今回は、「Release版」と「Debug版」、さらに「64 bit」と「32 bit」の4パターンで測定しました。各3分ずつ計測し、それぞれの平均速度と最終演算数は以下のとおりです。

VisualStudioのベンチマーク結果表。x64 Releaseが2447.92 MOPSで最速、Debug版との間に約5.8倍の性能差があることを示している。
 

最適化の力 Release と Debug

この表で一番驚くべきは、Release版とDebug版の速度差です。

  • x64版:Release版(2447.92 MOPS)は Debug版(417.10 MOPS)のなんと約5.8倍も高速です。

  • x86版:Release版(2037.16 MOPS)は Debug版(504.57 MOPS)の約4倍の速さでした。

これは、コンパイラの最適化機能がどれほど強力かを示しています。Debug版はデバッグ情報を付加するため、コードが最適化されません。一方、Release版は速度やサイズを最大限に最適化するようにコンパイルされるため、これほどの差が生まれるのです。

 

64 bitの優位性 x64 vs x86

次に注目したいのが、64 bit(x64)と32 bit(x86)の性能差です。

  • Release版:x64版(2447.92 MOPS)は x86版(2037.16 MOPS)より約20%高速です。

64 bit環境では、より大きな整数を一度に扱ったり、レジスタを効率的に使えたりするため、演算性能が向上します。今回のテストでも、その恩恵がはっきりと現れました。

 

ここでひとつ疑問が・・・

Debug版においては、64 bit(x64)より 32 bit(x86)のほうが優れたスコアになっている・・・?なぜ?なんとなく、Release版の結果からすると、64 bitアーキテクチャのスコアのほうが高くなりそうな雰囲気があるけど・・・。
ということでGeminiに聞いてみた。回答は以下の通り。

使用したプロンプト

#役割

あなたは知識豊富なシステムエンジニアです。

#タスク

開発をするために、環境を調査していて、自前のベンチマーク計測をしたところ、以下のような結果が出ました。


C:\Project\benchmark\x64\Release>benchmark.exe 3

=== Integer Benchmark for 3 minutes ===

[60.00 sec] Total ops=147252470000, Avg speed=2454.21 MOPS

[120.00 sec] Total ops=294397080000, Avg speed=2453.31 MOPS

=== Benchmark Complete ===

Total ops=440624870000, time=180.00 sec, Avg speed=2447.92 MOPS

Final sum=28641130847348264

C:\Project\benchmark\x64\Debug>benchmark.exe 3

=== Integer Benchmark for 3 minutes ===

[60.00 sec] Total ops=25025060000, Avg speed=417.08 MOPS

[120.00 sec] Total ops=50060460000, Avg speed=417.17 MOPS

[180.00 sec] Total ops=75077450000, Avg speed=417.10 MOPS

=== Benchmark Complete ===

Total ops=75077450000, time=180.00 sec, Avg speed=417.10 MOPS

Final sum=4880121880399640

C:\Project\benchmark\Release>benchmark.exe 3

=== Integer Benchmark for 3 minutes ===

[60.00 sec] Total ops=122695500000, Avg speed=2044.92 MOPS

[120.00 sec] Total ops=244800140000, Avg speed=2040.00 MOPS

=== Benchmark Complete ===

Total ops=366689300000, time=180.00 sec, Avg speed=2037.16 MOPS

Final sum=23835232499750960

C:\Project\benchmark\Debug>benchmark.exe 3

=== Integer Benchmark for 3 minutes ===

[60.00 sec] Total ops=30213200000, Avg speed=503.55 MOPS

[120.00 sec] Total ops=60381080000, Avg speed=503.18 MOPS

=== Benchmark Complete ===

Total ops=90822260000, time=180.00 sec, Avg speed=504.57 MOPS

Final sum=5903552907741872


なぜDebugはx86(32 bit)のほうがベンチスコアが上なのか考察してください。

Gemini考察

提供されたベンチマーク結果を分析すると、Debugモードでのx86(32 bit)版がx64(64 bit)版よりも高いベンチマークスコアを出しているのは、レジスタの利用効率が原因である可能性が高いです。

デバッグモードと最適化

まず、デバッグビルドでは、一般的にコンパイラの最適化が無効化されています。これは、デバッガがステップ実行や変数の値を確認できるようにするためです。最適化が適用されない場合、コンパイラはコードをより忠実に機械語に変換します。

x86とx64のレジスタ利用

  • x64(64 bit)

    64 bitのレジスタ(RAX, RBXなど)と、x86にはない追加のレジスタ(R8-R15)が利用できます。理論的には、これにより一度に多くのデータを扱え、パフォーマンスが向上します。しかし、デバッグモードでは、これらの64 bitレジスタをフルに活用するためのコンパイラの最適化が行われません。また、関数呼び出し規約によっては、スタックに引数をプッシュする際のオーバーヘッドがx86よりも大きくなる場合があります。

  • x86(32 bit)

    32 bitのレジスタ(EAX, EBXなど)を使用します。レジスタの数が少ないため、スタックへのデータの出し入れ(スピル)が頻繁に発生します。しかし、ベンチマークプログラムのような特定のタスクでは、32 bitレジスタでの演算が命令キャッシュに効率よく収まることがあります。また、32 bit環境の関数呼び出し規約はシンプルで、デバッグモードでも効率的な実行が期待できる場合があります。

考察

このベンチマーク結果では、Debugモードという最適化がオフの状態で、x64版がx86版のパフォーマンスを上回れていないことがわかります。これは以下の理由が考えられます。

  1. レジスタ利用の非効率性

    最適化されていないx64コードは、多くのレジスタがあるにもかかわらず、それらを効率的に使いこなせていない可能性があります。例えば、必要以上に64 bitレジスタを使ってしまい、レジスタ間でのデータの移動が増えているなどが考えられます。

  2. メモリ配置と命令キャッシュ

    32 bitコードは一般的に64 bitコードよりもサイズが小さくなります。これにより、命令キャッシュへのヒット率が高まり、メモリアクセスの待ち時間が減って実行速度が向上する可能性があります。

  3. 呼び出し規約のオーバーヘッド

    x64版の呼び出し規約がデバッグモードの特定のコードパスでオーバーヘッドを引き起こしている可能性があります。特に、引数の受け渡しやスタックフレームの管理が、x86のシンプルさと比較して非効率になっているのかもしれません。



 

まとめ

Debug / Release ビルド、x86 / x64 の設定で、これほどの違いが出ました。この最適化を起因とする実行処理の違いが、リリース後の不具合や、開発環境で再現できない問題などに直結してくるのかもしれません。きっと、バックグラウンドで実行されているプロセスによる負荷によっても、結果はまた変わってくるのでは?と考えられます。

また、環境によっては、64 bit環境より、32 bit環境のほうが優れている場合があるということにも驚きでした。必ずしも64 bit環境が優れているわけではなく、理にかなった設計や組み合わせが重要ということが理解できました。

この他にも、CPUの32 bit/64 bit、OSの32 bit/64 bitなどそういった組み合わせでも性能差が出てくる可能性があります。なかなかイマドキWindowsの32 bit版を手に入れるのも難しいかもしれませんが、Visual Studioに限らず、Raspberry Piなどでも同じような検証をしたら、何かの参考になるかもしれませんね。

次回は、「組込み環境」や「Windowsでは重すぎてOSを書き換えて使用するようなPC」でも性能を調べたい!という欲望から生まれた、自作プログラムによる検証結果を公開します。様々な環境での処理能力の違いが気になる方は、ぜひご覧ください。

【連載】自作コードでC言語のベンチマーク計測#2 ~ Linux環境でも計測してみる ~

既存のベンチマークソフトでは計測が難しい組込みLinuxや古いPCの性能調査。自作のC言語プログラムを使い、OSの書き換えや仮想環境、RISC-V搭載ボードなど、多様な環境での処理能力を横断的に比較検証した結果を解説します。