[ADP開発日誌-公開1周年記念特集 Part4] プログラミング言語の制御構造のいろいろ(2)

前回からちょっと間が空いてしまいましたが、ADPの1周年記念記事のPart4です。

関数呼び出しのスタックの使われ方


前回の記事の終わりにスタックという言葉が出てきましたが、スタックとはプロセス(正確にはスレッド)毎に用意されているメモリエリアで、関数呼び出しやローカル変数の保持に使われます。

以下のC言語での関数呼び出し時のスタックの使われ方の例を図1に示します。
func( arg1, arg2, arg3); /* ------- ※1 */

         図1


スタックは伝統的にアドレスの上位(数字が大きい)から下位に向かって領域が確保されます。
※1の関数が呼び出されるとき、先ず引数がスタックに積まれ、次いでリターンアドレス、そしてローカル変数の領域が確保されます。関数というのはどこから呼び出されても元の場所に戻ることが出来ますが、それが実現できるのは、呼び出し後に実行すべき命令のアドレス(リターンアドレス)をスタックに保持しているからです。
また、同時にどこから呼び出されてもローカル変数が『関数内で一時的に有効な変数』として機能できるのもスタックに変数のエリアを確保しているからになります。

ちなみに、数年前に流行したセキュリティリスクでバッファオーバーランというものがありますが、これはローカル変数の領域を溢れさせアドレスの上位にある戻りアドレスを書き換えてウイルスのプログラムを実行しようというC言語の関数呼び出しの仕組みを悪用したものになります。現在ではCPUレベルでの対策(NXビットとかXDビットとか呼ばれものでデータ領域の実行の禁止)が行われ、バッファオーバーランの脆弱性が起こりにくくなっています。

スタックには引数が積まれていますが、引数が積まれる順番には2通りのやり方があります。図1ではリターンアドレスに次いで arg1,arg2,arg3 と積まれていますが、反対に arg3,arg2,arg1 というやり方もあります。arg3,arg2,arg1の順番ですが、一見すると反対に見えますが、スタックに積む順番はarg1,arg2,arg3となります。ややこしいですが、※1の擬似アセンブラコードを示すと意味が良く分かるかと思います。

	※2 ※1の擬似アセンブラコード(cdecl呼び出し)

	PUSH arg3
	PUSH arg2
	PUSH arg1
	CALL func

PUSH命令の発行順とスタック上のリターンアドレスから見た順番が反対になります。
関数の呼び出し方法(つまりどのように機械語に翻訳するか)を呼び出し規約(主にx86のCPUで用いられている表現)といい、※2のような呼び出し方法をcdeclと呼びます。呼び出し規約はその他にPASCAL(文字通りPASCALで採用されている)とかstdcall(Windows-APIで採用)とかthiscall(C++のメンバ関数呼び出し)等があります。

メンバ関数の呼び出しでのスタックの使われ方


続いて、C++のメンバ関数呼び出しでのスタックの使われ方について説明します。
以下のC++でのメンバ関数の呼び出し時のスタックの使われ方の例を図2に示します。
object.method( arg1, arg2, arg3); // ------- ※3

            図2



※3の擬似アセンブラコードを以下に示します。

	※4 ※3の擬似アセンブラコード(thiscall呼び出し)

	PUSH arg3
	PUSH arg2
	PUSH arg1
	PUSH object
	CALL method

違いは、object(正確にはobjectのアドレス)がthisポインタとして引数の一つとしてスタックに積まれていることです。その他の違いはありません。こうしてみるとオブジェクト指向というのは単純に

method( &object, arg1, arg2, arg3)

というコードを、

object.method( arg1, arg2, arg3)

という風に記述できる構文上の違いであるに過ぎないということに気づくかと思います。
ADPでは、この考え方を推し進めて、メソッド形式(メンバ関数呼び出しとほぼ同じ意味)として通常の述語形式での呼び出しとメソッドの呼び出しを混ぜて使うことができるようにしています。

ちなみに、私も含めて、多くのC言語の上級エンジニアがこのような見方をしてC言語からC++(オブジェクト指向)に移行していたかと思います。

もっとも、この話は、『仮想関数はどのように機械語に翻訳されるのか?』の話をしなければ終わりになりません。
次いで、仮想関数の呼び出しの話をします。

仮想関数の呼び出しでのスタックの使われ方

前節で説明したメンバ関数の呼び出しは従来の関数呼び出しの延長線上のものですが、ここでは、仮想関数と呼ばれるオブジェクト指向独特の呼び出し方法について説明します。ちなみに仮想関数の説明自体は省略します(コメント欄でリクエストを頂ければ記事を追加するかもしれません)。 仮想関数の説明は次の記事で行います。

以下の仮想関数の呼び出しについて考えます。ちなみにスタックの構成は図2で仮想関数・通常のメンバ関数(非仮想関数)での違いはありません。
object.virtual_method( arg1, arg2, arg3); // ------- ※5
※5の擬似C++コードを以下に示します。

	※6 ※5の擬似アセンブラコード(thiscall呼び出し)

	PUSH arg3
	PUSH arg2
	PUSH arg1
	PUSH object
	MOV	 EAX, [object + vptr] ; ------------------- A
	MOV	 EDX, [EAX + virtual_method_offset] ; ----- B
	CALL EDX ; ------------------------------------ C

object + vptrなどや、EAX + virtual_method_number の部分がかなり曖昧ですが、エッセンスとして読んでいただければと思います。
※6のアセンブラコードではよく分からないかと思いますので、まずはオブジェクトのメモリレイアウトを図3に示します。

            図3



vtableと呼ばれるテーブルに呼び出すべき仮想関数の場所(アドレス)が格納されています。
また各objectはvtableの場所(アドレス)を保持する変数(ポインタ)を持っています。
さらに、機械語の特徴のとして関数呼び出し(CALL命令)は、常に同じ場所(アドレス)の関数を呼び出すだけでなく、変数(レジスタ)を通して間接的に呼び出すこともできるようになっています。

以上を踏まえて再度、擬似アセンブラコードを説明しますと、

Aでは、vtableを参照しています。EAXとはレジスタというCPUが持っている変数になりますがそこへvtableのアドレス(vptr)を代入しています。[] というのはアセンブラでのポインタ参照(間接演算子 *)になります。

Bでは、virtual_methodの呼び出すべきアドレスを、EDXに代入します。このvirtual_method_offsetですが配列のインデックスのようなもので、図3では0ということになります。

最後のCのCALL命令が、A,Bを通して取得した呼び出すべき仮想関数の呼び出しを行っていることになります。

このように擬似アセンブラコードを通してみますと、説明は難しいですが、たったの2命令の追加で仮想関数呼び出しを実現しており、C++での仮想関数呼び出しというのはかなり効率的であることが分かります。

もともと、私はアセンブラが大好き(ハードウェアを直接制御できるので)だったのですが、時代に押されてC言語を使うようになりましたが、その理由の一つとしてC言語が高級アセンブラとして設計された(つまりこのように簡単にアセンブラに置き換えられる)から動作がよく理解しやすい面があったからで、その設計思想はC++にも引き継がれていることが分かります。

続いては、公開1周年記念特集記事として『プログラミング言語の制御構造のいろいろ(3)』を書いてみます。
2011-08-04 | コメント:0件



[ADP開発日誌-公開1周年記念特集 Part3] プログラミング言語の制御構造のいろいろ(1)

ADPの1周年記念特集のPart3です。『プログラミング言語の制御構造のいろいろ』ということで数回にわたって記事をアップします。ちなみに本日でちょうどADPの初回リリースから1年になります。
「なぜ、制御構造?」と思われるかもしれませんが、それはADP(Prolog)が持っている制御構造(バックトラック)が独特のものということと、JavaScriptやRubyにありますクロージャが本格的に普及してきて私自身が持っている制御構造に対する考え方(というか感覚)を変える必要があるので記事にしてみます。

制御構造とは

制御構造とはプログラムの流れ、広くはその命令(for文とかif文)を指します。制御構造を有名なものにしたのは、かのダイクストラ氏が提唱した構造化プログラミングがあります。今となっては『構造化プログラミング』という言葉を始めて聞いた人もいらっしゃるかと思いますが、『構造化プログラミング』が提唱された後に、今ではおなじみの制御構造文
・選択(if)
・反復(for,while等のループ)
が明確になりました。それまでの言語ではif文やfor文もありましたが充分でなく、本格的なプログラムの記述にはgoto文を使う必要がありました。そのれに加えてgoto文では様々なプログラムの流れを作ることが出来、流れの追いにくいいわゆるスパゲティプログラムというものもありました。私が駆け出しの頃(20年程前)にはよく可読性の悪いプログラムに対して『このスパゲティプログラムが~』という表現を聞いていました。

機械語ではどうしているのか?

なぜ、「機械語の話が出てくるのか?」と思われるかもしれませんが、制御構造の発展の歴史のルーツを探ることと、コンパイラ言語では制御構造が機械語に変換されるのでその仕組みを探るという意味で、続いて機械語の話をします。
機械語では初期のプログラミング言語のように比較文(if文)とgoto文のみで制御を行います。今となっては逆に難しいかもしれませんが、for文やwhile文がなくてもif文とgoto文の組み合わせでループを記述することが出来ます。
意外に思われるかもしれませんが、もう一つの制御構造文である関数呼び出し(サブルーチン呼び出し)も機械語にCALL命令という形で存在します。初期のCPUにはCALL命令がないものもあったらしいですが、今われわれが主に使っているパソコンのx86と呼ばれるCPUにもCALL命令があります。さらにx86の先祖をたどりますと、8080というパソコン用の8ビットCPUがありますが、そのCPUにもCALL命令があります(それから先は8008、4004とたどれますがこれらにCALL命令があるかどうかは不明です・・・)。
もちろんCALL命令が関数呼び出しとイコールではありません。CALL命令と関数呼び出しの違いは引数の受け渡しになります。CALL命令には引数の概念がありません。引数の受け渡しはレジスタまたはスタックまたはグローバル変数ということになります。C言語の関数呼び出しが機械語に翻訳されるるとCALL命令に翻訳されますが、その引数はスタックで渡されます。

続いては、公開1周年記念特集記事として『プログラミング言語の制御構造のいろいろ(2)』を書いてみます。
2011-07-30 | コメント:0件



C++/STLで日本語メール送信(base64)

地震でここしばらくブログの更新を自粛しておりましたが、自分でできることをやるのが一番ということで、ブログの更新を再開します。
 
ブログビューワーのリリースを行おうかとおもっとりましたが、その間にADPもアップデートがあったのと、その中で、久しぶりにC++/STLネタができたので、今回はC++/STLネタを披露いたします。
ちなみに、このブログで一番アクセス数が多い記事は、C++/STLでCSVファイルの読み込みで、一時期、自作機向けWindows7のKernel-Power 41病対策のアクセス数が首位に躍り出たのですが、再びCSVが首位に返り咲きました。『おまえらどれだけCSVやねん』と突っ込みたくなるのですが、次の刺客ということで、日本語メール送信を送り込みます。
コードは長くなるので呼び出し部分のみ以下に示します。全ソースのダウンロードは、プラットホーム別に以下のとおりです(ソースの内容自体は同じでプラットフォームに合わせて日本語のエンコードを変えています)。
 
Windows版(Shift_JIS)
Linux版(utf-8)
 
int main(void)
{
#ifdef _WIN32
        WSADATA	wsaData;
        WSAStartup( 0x0101, &wsaData);
        string	charset = "Shift_JIS ";
#else
        string	charset = "utf-8";
#endif

        string	smtpserver("mail");
        string	to("to_address@example.com");
        string	cc("");
        string	bcc("");
        string     from("from_address@example.com");
        string	subject("サブジェクト");
        string	text("メール本文");

        cout << subject << ":" << text << endl;

        if ( !sendmail( smtpserver, to, cc, bcc, from, subject, text, charset) ) {
                cout << "Error Send Mail.";
                return 1;
        }

#ifdef _WIN32
        WSACleanup();
#endif
        return 0;
}
 
各パラメータに値をセットして、sendmail関数を呼び出します。あて先(to,cc等)が複数ある場合はコンマ , で区切ります。サブジェクト(件名)とメールの本文のみ日本語OKです。使用している文字コードをcharsetで指定します。
例では、WindowsがShift_JISで、それ以外がutf-8になっていますが、Windowsでutf-8を使うことも可能です。
(もちろんメールの件名や本文で使用する文字コードをutf-8にする必要があります)。
 
日本語の取り扱いについてですが、昔、日本語メールと言えば、JISコードに変換して送るという風にやっておったのですが、最近のメーラは適切に処理をすれば(文字コードを指定し、base64等で適切にエンコードする)文字コードを変換する必要がなくそのまま送信できるようです。このプログラムもそのまま送信しています。
ちなみに、このプログラムにはbase64のエンコード・デコードも入っています。詳しくはソースをご覧ください。
 
コンパイルに際して注意があります。char をunsigned charとしてコンパイルする必要があります。
gccでは、
g++ -funsigned-char sendmail.cpp
のように -funsigned-char オプションを使用してコンパイルします。
Visual Studio 2008の場合は、プロジェクトのプロパティ→構成プロパティ→C/C++→言語→char型を規定でunsigned を はい にします(または/Jオプションを指定します)。
 
このプログラムは、以下の環境で動作確認しました。
 Linux:Centos 5.5
 Windows: Visual Studio 2008 professional / Windows 7 ulitimate 64bit (32ビットモードでコンパイル)
2011-03-29 | コメント:2件



プログラミング言語の人気ランキング

不覚にも風邪をひいてしまい先週から更新が滞ってましたです。
以前は何カ月も更新せずにほっておいたのですが、最近更新しないとアクセス数が減るのである種の脅迫観念にとらわれたりしますです。

風邪をひいていたのでネタもあまりなく先週に引き続き他の記事の引用で。以下の記事によりますと話題のプログラミング言語第一位はJavaらしい。

プログラミング人気ランキング、Smalltalkトップ50から消える

はやくADPもランクインしたいのだが・・・というお約束は置いておいて、JavaとCで話題が二分しているらしいです。JavaとCであれば作るものに応じて住み分けができているかと思われます。つまり、JavaだとWEBアプリで、Cだとシステムよりのツール等という感じになるでしょうか。なのでこの2つの言語はそれぞれ順位を保っていくのではないでしょうか。またC++が3位というのが興味深かったりします。ちなみに私ですが、ここ10年程C++の仕事を受けたことがありません。もちろん使えはしますが、C++を使った最後の請負仕事は12年程前でMFCでGUIのプログラムの作成だったりします。もっともCの方はもっと前になりますが・・・。と思いきや最近、久しぶりにC++の仕事をしました。
もちろん自社で使うツールなどをC++やCを使ってというのはあるのですが・・・。Javaに関しては5年程前にある人に教えて以来ということになります。一時はJavaPressに記事を書いていたこともあったのですが・・・。
とまぁ私の仕事事情(体感)とちょっと違うのですが、2010年現在のITエンジニアがマスターすべき言語は、Java、C、C++ということのようです。ってホンマかどうかは置いておいてこのうちの1つはマスターしておいた方が良さそうです。

このブログですがC++ネタを上げているのですが、確かにC++を使う方の人口が多いみたいで、ちょいちょいアクセスがあます。ちなみにダントツの人気記事は、C++/STLでCSVファイルの読み込みだったりします。『21世紀も10年が過ぎ去ろうというところで、C++でCSVっているの?』と思わなくもないのですが、まぁ人気があります。ちなみに、『【求人募集】GIGAZINEのために働いてくれる記者・編集を募集します』を読んで思うこと。はさすがにGIGAZINE人気に乗っかって先週はアクセスを伸ばしたのですが、今週はぱったりとやんでしまいました。
2010-08-09 | コメント:0件



C++/STL 1GBのintのソートにかかる時間 2010

未踏の説明会の続きですが、説明会の中に技術的なセッションもありまして、グーグル株式会社のソフトウェアエンジニア 鵜飼さんの講演が面白かったのですが、その中で、『1GBのintのソートにかかる時間は、封筒の裏計算で、30秒』というのがありました。

パフォーマンスには一家言ある私ですが、さすがに1GBのintのソート時間にはピンと来ませんでした。
という訳で、ホントかどうかやってみました。


#include <vector>
#include <algorithm>
#include <iostream>
#include <time.h>

using namespace std;

int main(void)
{
    vector<int>        values;

    srand(time(0));
    // vectorに適当な値を入れる
    for ( int i = 0; i < 1024*1024*1024 / sizeof(int); i++ ) {
        values.push_back((int)(rand()*rand()-i));
    }

    // ソートする
    clock_t        t = clock();
    sort( values.begin(), values.end());
    cout << "Time(sort) is "
         << (double)(clock() - t) / CLOCKS_PER_SEC << "sec." << endl;

    return 0;
}


実行時間(Core i7-920 Windows7 コンパイルVC++2008 リリースモード 64ビットモード)は以下になります。上記のプログラムですが、32ビットモードでは動作しません。32ビットプロセスはリニアに1GBのメモリは確保できないです。

Time(sort) is 43.895sec.

なるほど、確かに30秒からそう離れていません。
ちなみに、この手の封筒の裏計算ですが、桁が違わなければOKと考えてよいでしょう。なので、細かい値の違いが問題になる場合は、実アプリでキチンとベンチマークをとるのがよいでしょう。
この手の結果の受け止め方ですが、おそらく一般の業務アプリを作成する人にとっては『理論的限界値』程度に思っていた方がよいでしょう。つまり

 1秒間に数百万個のint型のソートができる。数千万個になったら要注意。

と思っておけばよろしいかと思います。実際に私の経験でも行数が数百万件のソートをSQLで行うのはあまり問題になることはなかったです。(もちろんメモリが十分にあればの話ですが)。

実行時間の詳細ですが、説明では以下のとおりでした。
 ・要素を比較する回数(ソートのオーダnlogn)から、
  2^28 * log(2^28) → 2^28 * 28 → 2^28 * 2^5 → 2^33(2の33乗)回
 ・比較に際してのL1キャッシュのアクセス時間 0.5ns / 回
 ・比較に際してのブランチペナルティ 2.5ns / 回(2回に1回ペナルティがあると仮定する)

 実行時間 2^33 * (0.5 + 2.5)nsec = 25.76sec 約30秒

ただ、上記の計算ですが、ブランチペナルティが全体の速度を決定しているというのはいささか疑問があります。上記の場合、メモリのアクセス回数から計算した方が良いのでは?と思います。
つまり、
 ・要素を比較する回数(ソートのオーダnlogn)から、
  2^28 * log(2^28) → 2^28 * 28 → 2^28 * 2^5 → 2^33(2の33乗)回
 ・ 比較に際してのメモリアクセス回数 2回(リード&ライト) 2*4バイト
 ・キャッシュライン 32バイト

 ・メインメモリへのアクセス回数 2^33 * 2 * 4 / 32 = 2^31 回
 ・メインメモリアクセス性能 1回のアクセス 10nsec(DDR3のレイテンシーから)

 実行時間 2^31 * 10nsec = 21.47sec

うーん、数値的には似たり寄ったりであまり変わらないか・・・・
2010-07-09 | コメント:0件
Previous Page | Next Page