コンパイラは数を当てるマジックを解けるか

「任意の数を思い浮かべてもらい、特定の計算をしてもらい、その結果を当てる」という子供向けのマジックがあります。これを C 言語のコンパイラで解いてみましょう。


動作確認環境

  • Windows 10 Home 21H1, 64bit
  • Visual Studio Community 2019

マジックの内容

このマジックは無限のバリエーションが作れますが、今回は次の計算をすることにします。

  1. 好きな数を思い浮かべてください。
  2. その数に 5 を足してください。
  3. 2 倍してください。
  4. 4 を引いてください。
  5. 半分にしてください。
  6. 最初に思い浮かべた数を覚えていますか。その数を引いてください。

結果は、最初に思い浮かべた数が何であっても、「3」になります。
理由は次の通りです。

((n + 5) × 2 - 4) / 2 - n
= (2n + 10 - 4) / 2 - n
= (2n + 6) / 2 - n
= n + 3 - n
= 3

「2」を思い浮かべた場合

最初に思い浮かべた数を「2」として、このマジックを C 言語で書き下します。

// MyMagic.c
#include <stdio.h>

int main()
{
    int     any;
    int     answer;

    any = 2;           // 1. 好きな数を思い浮かべてください
    answer = any + 5;  // 2. その数に 5 を足してください
    answer *= 2;       // 3. 2 倍してください
    answer -= 4;       // 4. 4 を引いてください
    answer /= 2;       // 5. 半分にしてください
    answer -= any;     // 6. 最初に思い浮かべた数を引いてください

    printf("%d", answer);

    return 0;
}

「cl /O2 /Fa MyMagic.c」でコンパイルします。「/O2」は速度優先で最適化、「/Fa」はアセンブリファイルを出力、の意味です。
どのようにコンパイルされたか、アセンブリファイルを見てみます(関係する箇所のみ抜粋・編集)。

_main PROC
    push 3
    push <"%d" へのポインタ>
    call _printf

何の計算もしない、単なる「3」 を表示するプログラムに最適化されました。

任意の数を受け取る場合

思い浮かべた数を実行時に入力できるようにしてみましょう。scanf 関数を使います。

// MyMagic.c
#include <stdio.h>

int main()
{
    int     any;
    int     answer;

    scanf("%d", &any); // 1. 好きな数を思い浮かべてください

    answer = any + 5;  // 2. その数に 5 を足してください
    answer *= 2;       // 3. 2 倍してください
    answer -= 4;       // 4. 4 を引いてください
    answer /= 2;       // 5. 半分にしてください
    answer -= any;     // 6. 最初に思い浮かべた数を引いてください

    printf("%d", answer);

    return 0;
}

「cl /O2 /Fa MyMagic.c」でコンパイルし、実行し、思い浮かべた数として「12345」を入力します。

C:\tmp>MyMagic.exe
12345
3

予想通り、「3」と表示されました。
どのようにコンパイルされたのでしょうか。アセンブリファイルを見てみます (関係する箇所のみ抜粋・編集) 。

_main   PROC
    push    <変数 any のアドレス>
    push    <"%d" へのポインタ>
    call    _scanf

    push    3
    push    <"%d" へのポインタ>
    call    _printf

ユーザーからの入力は受け付けるものの、その値は無視し、固定値「3」を表示するプログラムにコンパイルされました。

以上、数を当てるマジックをコンパイラの最適化で解く話でした。