低レベルプログラミング(No.09:C#について)

使用ツール:Visual Studio Community 2019
使用言語 :C言語/C#言語

C#について

今回からは、主にC#(言語)について取り扱って行こうと思いますが、まず、その前にC#について思う所を書いていきたいと思います。

C#は遅い?

よく聞くフレーズ

僕がGUIが必要なアプリ開発を請け負う際には、「C#で開発したいのですがよろしいでしょうか?」と発注元に確認します。

理由は以下になります。

  • .NET framework(もしくは.NET core)上で開発が出来る
    • GUIコントロールの扱いが容易
    • メモリ管理を.NETに任せられる
    • 多くの便利な3rdParty
  • C言語(使い慣れた言語)に近い使い方が出来る
  • C#の便利な機能が利用出来る
  • 以上により、開発期間を短縮できる

たいてい、発注元の担当者様は「問題ありません。」と答えてくれますが、よく以下のフレーズを聞きます。

「処理時間が掛かる様だったらC言語でお願いします。C#は遅いから

いつも、自分はこれを聞くと頭の中に「?」が浮かびます…。

遅いイメージの理由を推測

「C#(で作成したプログラム)は遅い」というイメージには、心当たりがないわけではありません。

① 中間言語が作成される

C#のソースコードをコンパイル(ビルド)すると、実行ファイル(EXEファイル)が作成されますが、実行ファイルはx86 CPU向けの機械語で構成されている訳ではなく、共通言語ランタイム(.NETで提供される)上で動作する中間言語で構成されます。

前回の記事(「No.08:TIPS(C)」で紹介した逆アセンブルのためのツール(dumpbin.exe)を使っても、逆アセンブルは出来ません。

C#(.NET)で作成した実行ファイルの逆アセンブルを行うには、ildasm.exeを利用します。(ildasm…Intermidiate Language DisASseMblerの略でしょうか>)

ildasm.exe 実行結果

何となくx86 CPU向けのアセンブリコードにも似てますが、実際にはかなり違います。

共通言語ランタイム(CLR)上で動作する

実行ファイルがCPU(x86)向けの機械語ではないので、実行する際のイメージとしては以下の様な感じを想像してしまいます。

確かに、間にCLRが挟まっているので、いちいち中間言語→x86機械語へ変換しながら動いているイメージがありますが、そんな昔のBASIC言語の様なインタープリターの様な動作をしているのでしょうか?

そのような事をすれば、CPUの機能(PipelineやSIMD命令など)を全く使えない事になります。

マイクロソフト社のドキュメントには下記記述があります。

クラスの検索と読み込み、メモリ内でのインスタンスのレイアウト、メソッドの呼び出しの解決、ネイティブ コードの生成、セキュリティの強化、およびランタイムのコンテキスト境界の設定

https://learn.microsoft.com/ja-jp/dotnet/standard/clr

通常、.NET アプリは、中間言語 (IL) にコンパイルされます。 実行時に、Just-In-Time (JIT) コンパイラによって IL がネイティブ コードに変換されます。

https://learn.microsoft.com/ja-jp/windows/uwp/dotnet-native/

実際には、CLRはプログラムの実行時に、中間言語のプログラムをx86上で動作するネイティブなプログラムに変換し、その変換されたネイティブコードを実行しています。

なので、共通言語ランタイム(CLR)上で動作するから(C#で作成されたプログラムは中間言語だから)遅いというのは思い込みです。

③ 低レベルなアクセスが出来ない?

C言語では、ヒープ領域に確保したメモリへ配列形式、ポインタ形式でのアクセスが容易に実装できます。

C#では『メモリ管理はCLRに任しているので、C言語の様にはアクセスできない』というイメージがあるかも知れませんが、手順に従えばポインタを使ったメモリアクセスも可能です。

ポインタアクセスが出来るから高速な処理が出来るという訳でもありませんが…

④ 高機能な故に

C言語は機能が限られている事もありCソースコードとアセンブリコードの対応がとりやすくなっていました。

C#(に限らず、最近のオブジェクト指向言語)は、プロパティ、演算子のオーバーロード、カプセル化(ブラックボックス化)、高機能なメソッドの提供などにより、C#ソースコードの各行がどのようなアセンブリコードになりそうか、どの程度の実行コストがかかりそうかが想像しづらくなっています。

実行時のコストが想像しにくいので、基本に忠実にコーディングすると『なんだか遅いな』という印象を受けるのかも知れません。

ただ、『C言語だから高速である』という事はありません。C言語でも何も考えなければ処理は遅くなります。

C#は遅くない

あくまでも個人的な意見ではありますが、「C#は遅くない」と思います。

ただし、これまでの記事でC言語とアセンブリコードの対応を確認してきた様に、C#のソースコードが最終的にどのように機械語(ネイティブコード)に変換されるのか?実行コストが高い書き方になっていないかなどを気にしながらソースコードを作成する必要があります。

これは、どの言語にも言える事ではありますが…。

C#でよく利用される型

C#でよく利用される型のサイズと取りうる範囲は下記となります。

サイズ(byte)最小値最大値
byte10255
short2-32,76832,767
int4-2,147,483,6482,147,483,647
long8-9,223,372,036,854,775,8089,223,372,036,854,775,807
float4-3.402823E+0383.402823E+038
double8-1.797693E+3081.797693E+308

これらの値は覚える必要はなく、最小値は「int.MinValue」、最大値は「int.MaxValue」の様に扱う事が出来ます。(ただ、どの程度の値かは、なんとなしにでも覚えておくのは良いかと思います)

C#のメンバー

C#のクラスのメンバーは下記の区別があります。

(マイクロソフト社のC#プログラミングガイドからの一部抜粋です。)

メンバー           説明
フィールドフィールドとは、クラス スコープで宣言される変数です。 フィールドは、組み込みの数値型であったり、別のクラスのインスタンスであったりします。
定数定数とは、コンパイル時に値が設定され、設定された値を変更できないフィールドです。
プロパティプロパティはクラスのメソッドで、そのクラスのフィールドのようにアクセスされます。 プロパティは、クラスのフィールドを保護し、オブジェクトが認識することなくフィールドが変更されるのを防止できます。
メソッドメソッドは、クラスが実行できるアクションを定義します。 メソッドは、入力データを提供するパラメーターを受け取り、パラメーターを通じて出力データを返すことができます。 メソッドは、パラメーターを使用せずに値を直接返すこともできます。
マイクロソフト社のC#プログラミングガイドから一部抜粋

簡単なコードで示すと以下の様になります。

    class SampleClass
    {
        // フィールド 
        private int count = 0;

        // 定数 (実行時変更付加)
        private const int MAX_COUNT = 100;

        // プロパティ
        private int Count                  
        {
            get
            {
                return count;
            }
            set
            {
                if (value < 0)
                {
                    count = 0;
                }
                else if(value > MAX_COUNT)
                {
                    count = MAX_COUNT;
                }
                else
                {
                    count = value;
                }
            }
        } 

        // メソッド
        private int GetCount()
        {
            return count;
        }
    }

僕は、「メンバー」と言うと、上記の「フィールド」の事を思い浮かべてしまいますが、クラスを構成する要素が「メンバー」であり、それぞれについてきちんと分類されています。

Disposeメソッドについて

C#でいろいろなクラス(のインスタンス)を使用していると、Disposeメソッドを持ったクラスを見かけると思います。(例えばBitmapクラスもDisposeメソッドがあります)

Disposeメソッドがどのような物かは、マイクロソフト社のWEB上のドキュメントに記載があります。

Dispose メソッドを実装するのは、主にアンマネージ リソースをリリースするためです。 

(略)

.NET のガベージ コレクターは、アンマネージド メモリの割り当てや解放を行いません。

https://learn.microsoft.com/ja-jp/dotnet/standard/garbage-collection/implementing-dispose

C#プログラムはCLR(.NET)上で動作するので、その中で使用されるクラスなどはマネージ・リソース(CLRが管理するメモリリソースなど)となるので、ガベージ・コレクションで適切に解放されます。

しかし、C#プログラムの中から、システムや3rdパーティ製のライブラリの機能を利用した際に、アンマネージコード(C言語で作成されて実行されるコード)で確保されたメモリ等はアンマネージ・リソース(CLRが管理しないリソース)となり、そういったメモリリソースは、ガベージ・コレクションで解放されません。

という事は、Disposeメソッドを持つオブジェクトを使った際に、Disposeメソッドを適切に呼び出さないと、メモリ・リークするという事ですね…

実行時間が短いプログラムであれば、プログラム終了と共にそのプログラムに割り当てられてたメモリはシステムにより解放されますが、長時間稼働し続けるプログラムでは致命的なバグになりかねません…。

利用するオブジェクトのDisposeメソッドを適切に呼び出すのは元より、自身がアンマネージ・リソースを利用する場合はクラスにDisposeメソッドを実装する事を忘れない様にする必要があります。

※アンマネージ・リソースには、ファイルのハンドル等も含まれますが、メソッド内でOpen~Closeで閉じていれば、Disposeメソッドは不要です。ただし、フィールドでハンドルを保持しつづける様なクラスの場合はDisposeメソッドは必要になるでしょう。

まとめ

今回は、C#を扱うにあたって、自分が気になっている部分をメインに記載しました。

.NET(Framework, core)上で動作するプログラムになるという点が、C言語とは大きく異なっていました。

扱いやすい部分がある反面、気を付けなければならない部分もありました。

次回の予定

次回はC#によるポインタ操作について記載したいと思います。

(通常、ポインタを使う必要性は少ないと思いますが…)

また、次回からはVisal StudioのIDE(統合開発開発)を使っていきたいと思います。
(ソリューション全体のフォルダサイズが大きくなってしまうのが難点ですが…)

前の記事次の記事
No.08:TIPS(C)No.10:C#でポインタ