💬

[C#5] インストール不要のC#コンパイラ

2024/11/08に公開

Windows にもとから付属しているC#コンパイラを使って, C# をちょこっとお試しするための環境構築とサンプルプログラム(モンテカルロシミュレーションの並列化)

0. 概要

以下の内容について記載する.

  1. Windows付属のC#コンパイラ
  2. vscode の設定
  3. サンプルプログラム

個人的な趣味レベルでちょこっとプログラミングするにはお手軽でいいと思う.

1. Windows付属のC#コンパイラ

Windowsにはインストール不要で無料のC#コンパイラが付属する.

1.1 場所

【パス】
C:\windows\Microsoft.NET\Framework\v4.0.30319
csc.exe

(同じフォルダには jsc.exe (JScript) や vbc.exe (VB.NET) なども存在する.)

1.2 バージョン

手元のものはC#5. 単純に csc.exe を実行した結果は以下のとおり(抜粋).

PS C:\Windows\Microsoft.NET\Framework\v4.0.30319> .\csc.exe
Microsoft (R) Visual C# Compiler version 4.8.9232.0
for C# 5
Copyright (C) Microsoft Corporation. All rights reserved.

This compiler is provided as part of the Microsoft (R) .NET Framework,
but only supports language versions up to C# 5,
which is no longer the latest version.
For compilers that support newer versions of the C# programming language,
see http://go.microsoft.com/fwlink/?LinkID=533240

「使えるバージョンはC#5で, 最新版ではない. 最新版についてはマイクロソフトのサイトを見てね.」との記載. とはいえC#5は非同期処理(async/await)まで導入されたバージョンなので, ちょっとプログラミングを試す分にはそれなりに使えると思われる. 機能の詳細については公式サイト解説サイトを参照.

2. vscode の設定

とりあえず個人的にはシンタックスハイライトができればよいので, 特に何も設定してない. プログラムのコンパイルや実行は vscode で設定するのではなくバッチファイルを用意して対応する.

プログラムのコンパイルには以下のファイルをソースと同じフォルダに作成しておく.

make.bat
C:\Windows\Microsoft.NET\Framework64\v4.0.30319\csc.exe %1

また, プログラムの実行のため, run.bat というファイルも用意する(内容については後述).

3. サンプルプログラム

試しに簡単なプログラムでC#を試してみる.

3.1 プログラムロジック

モンテカルロシミュレーションで円周率を求めるプログラムを考える.

第1象限の単位円の面積は \pi r^2 / 4 = \pi 1^2 / 4 = \pi / 4, 一方, 単位正方形の面積は1なので, 両者の比は

\dfrac{\pi / 4}{1} = \dfrac{\pi}{4}

x, yx,y \in [0,1) の一様乱数であるとすると, 点(x,y) が単位円に含まれる確率は上記の面積の比, すなわち \pi / 4 になる.

したがって, x, y を多数生成し, そのうち単位円に含まれる数を数えると円周率 \pi を求めることができる.

図の例ではサンプル5個のうち単位円に入っているのが4個なので, \pi の近似値は以下のように計算される.

\dfrac{\pi}{4} \sim \dfrac{4}{5} \Rightarrow \pi \sim 3.2

3.2 プログラムの概要

  • 単なるシミュレーションなので, コマンドラインプログラムとする
  • シミュレーション回数と並列化のスレッド数は変更できるようにする
  • 並列化は Parallel.For を使用する
  • 並列化しない場合との比較, 実行時間も出力する
  • プログラムの流れは以下のとおり
    • コマンドライン引数の読み込み, シミュレーション回数, スレッド数の設定
    • 結果を格納する配列を準備
    • 乱数シード(スレッド共通部分)は Environment.TickCount で取得(シミュレーション実施ごとに結果は異なる)
    • スレッド内で使用する乱数ジェネレーター, 一時変数などはできる限り各スレッド内で準備する
    • スレッド外の変数(結果を格納する配列)へのアクセスは最小限にする(そうしないと遅くなる)
    • 各スレッド内での乱数シードは簡易的に共通の乱数シードにループ番号を加えたものとする
    • 結果を出力
  • シミュレーション回数やスレッド数を変えて実行したり, 実行時間を計測する部分はバッチファイルで処理(powershell のコマンドを利用, 後述の run.bat を参照).

3.3 プログラムコード

シミュレーションを実施するプログラム:

pi_sim.cs
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

class Program{
  static void Main(string[] args){
    //パラメータ引数の読み込み
    //n_sim : シミュレーション回数
    //n_thr : スレッド数
    int n_sim, n_thr;
    if(args.Length==2){
      n_sim = int.Parse(args[0]);
      n_thr = int.Parse(args[1]);
    }else if(args.Length==1){
      n_sim = int.Parse(args[0]);
      n_thr = 1;
    }else{
      n_sim = 100;
      n_thr = 1;
    }

    //スレッドごとのサンプル数
    int n_sim_ = n_sim/n_thr;
    //n_sim_ = n_sim/n_thr;
    //端数調整(intの割り算で小数点以下が切り捨てられることを利用)
    n_sim = n_sim_ * n_thr;

    //Parallelのオプション指定
    var opt = new ParallelOptions();
    opt.MaxDegreeOfParallelism = n_thr;

    //結果等を保存する配列
    int[] n_hit = new int[n_thr];
    int[] thr_id = new int[n_thr];

    //シードの設定(TickCountは42.9日周期)
    int seed = Environment.TickCount;
    
    Parallel.For(0,n_thr,opt,idx=>
    {
      //スレッドごとの一時変数の宣言
      var rand = new Random(seed+idx);
      double x,y;
      int n_hit_=0;

      //シミュレーションの実施
      for(int i=0;i<n_sim_;i++){
        x = rand.NextDouble();
        y = rand.NextDouble();
        if(x*x+y*y<=1){
          n_hit_++;
        }
      }

      //結果の格納
      //(スレッド外の変数へのアクセスはスレッドの最後に一回だけ)
      n_hit[idx]=n_hit_;
      thr_id[idx] = Thread.CurrentThread.ManagedThreadId;
    });

    //結果の書き出し

    //シミュレーション件数
    Console.WriteLine("\ntotal: {0}, per thread: {1}"
      ,n_sim,n_sim_);

    //スレッドごとの結果
    for(int idx=0;idx<n_thr;idx++){
      Console.WriteLine(
        "Loop Index: {0}, Thread ID: {1}, # Inside: {2}, Estimated PI: {3}"
        ,idx
        ,thr_id[idx]
        ,n_hit[idx]
        ,(double)n_hit[idx]/(double)n_sim_*4.0);
    }

    // 全体の結果
    Console.WriteLine("\nEstimated PI: {0}\n"
      ,(double)n_hit.Sum()/(double)n_sim*4.0);
  }
}

シミュレーション回数の変更や時間計測をするバッチファイル:

run.bat
powershell -C "(Measure-Command {./pi_sim 100000000 | Out-Default}).TotalSeconds"
powershell -C "(Measure-Command {./pi_sim 100000000 8 | Out-Default}).TotalSeconds"

3.4 実行結果

1億回のシミュレーションを実施し, 単一スレッドの場合と8スレッドの場合を比較した(Windows11 / Core i7-1265U 1.80GHz).

> powershell -C "(Measure-Command {./pi_sim 100000000 | Out-Default}).TotalSeconds"

total: 100000000, per thread: 100000000
Loop Index: 0, Thread ID: 1, # Inside: 78544754, Estimated PI: 3.14179016

Estimated PI: 3.14179016

1.6156993

> powershell -C "(Measure-Command {./pi_sim 100000000 8 | Out-Default}).TotalSeconds"

total: 100000000, per thread: 12500000
Loop Index: 0, Thread ID: 1, # Inside: 9818126, Estimated PI: 3.14180032
Loop Index: 1, Thread ID: 3, # Inside: 9818755, Estimated PI: 3.1420016
Loop Index: 2, Thread ID: 4, # Inside: 9815393, Estimated PI: 3.14092576
Loop Index: 3, Thread ID: 5, # Inside: 9819066, Estimated PI: 3.14210112
Loop Index: 4, Thread ID: 6, # Inside: 9816838, Estimated PI: 3.14138816
Loop Index: 5, Thread ID: 7, # Inside: 9816178, Estimated PI: 3.14117696
Loop Index: 6, Thread ID: 8, # Inside: 9818250, Estimated PI: 3.14184
Loop Index: 7, Thread ID: 10, # Inside: 9815890, Estimated PI: 3.1410848

Estimated PI: 3.14153984

0.3498606

単一スレッドが1.6秒くらいであるのに対して8スレッドだと0.35秒くらいで4倍以上早くなっている.

Discussion