リンクフリーを近日中にとりやめる予定です

すでにリンクを貼っていただいている方、ご一報頂きたくお願い申し上げます。


ごく少数ですが、リンクをお断りする場合があります



ブログ内 風景光景カテゴリー

続編記事などをご希望の方は こちらへどうぞ

スポンサーサイト

上記の広告は1ヶ月以上更新のないブログに表示されています。
新しい記事を書く事で広告が消せます。

[未掲載分] 車輪の再発明 (8)

元来、好ましい意味で用いられない「車輪の再発明」。



筆者は、そのような行為を肯定的に捉えている。
なぜならば、新しい技術や文化が芽生えるきっかけは、一見無駄と思われることへの挑戦である・・・


お知らせ
活動休止にともない、この記事を事前に予約投稿してあります。
トップ記事の固定を目的としています


この題目は2012年暮れ頃に掲載しようと下書きし、諸事情により掲載却下とした分です。
前話、車輪の再発明 (7)にひきつづき、利用環境を OS はWindows , CPU は Intel 製、いわゆるウィンテル機を前提に綴ります。

今回は実質車輪の再発明 (6)の続き、「速度向上を目指し、データ転送部分を加工してゆく」に向けたお話。

一次資料を入手
この先、特定のハードウェア、機構に偏った部分、少々難しい話が出てくる。この類の分野に興味があるヒトは以下に挙げる資料を入手すると良いだろう。
たまに 64ビットや SSE/SSE2/SSE4/AVX などの情報を求めて検索サイト等を右往左往しているヒトもいるようで時間がモッタイナイ。転載や孫引きによる断片的な情報を辿るよりも原書を漁るのが近道となる。

日本語版に翻訳された資料を探すための検索用リンクを以下に貼る。
インテル 、日本語技術資料への検索リンク

インテルの日本語圏向け公式サイトにPDF ファイル形式にまとめられた文書が公開されているハズ。
ちなみに、コンピュータの発祥地はアメリカのペンシルベニア州。カリフォルニア州バークレーという説もあるが、いずれにせよ英語圏で牽引されて発展してきた。当然のことながら原文は英語版なので、文書の日本語訳や公開が遅れる可能性もある。

この中でタイトルに
IA-32 インテル アーキテクチャ ソフトウェア・デベロッパーズ・マニュアルと付いている上中下4巻が有益である。これらのファイルをダウンロードしておき活用すれば捗ることだろう。
PC に搭載されている Intel 製 CPU の共通事項や1970年代後半に登場した CPU から 第二世代の Pentium 4 に至るまで進化の流れを把握することができるだろう。中巻A と 中巻B の命令セットリファレンスにはアセンブリ言語を主軸にした解説となっているが、「同等のC/C++ コンパイラ組み込み関数」が併記されており、C/C++ 言語 で SSE/SSE2 を使いたい時には便利。
筆者の手もとにある資料の日付は2004 年となっている。2004 年頃は、Windows 上で動作するアプリを作るといえば 32ビットが暗黙の了解であった。その後拡張が施された、64ビットへの知識を補うならば
インテル エクステンデッド・メモリ64 テクノロジ・ソフトウェア・デベロッパーズ・ガイドの2巻分も参考になる。この2巻は32ビット版の解説と異なり、「同等のC/C++ コンパイラ組み込み関数」は併記されていない。

「エクステンデッド・メモリ64 テクノロジ」と呼ぶと長いので、略して「EM64T」もしくは「Intel64」と呼ばれている。



※ これは2012年7月 5日に掲載した記事の画像です

これを書いた時点で 64ビット版の OS が浸透しつつあるが、ここに至るまで紆余曲折があった。
おおざっぱに言えば、新築と増改築との違いを想像すると判りやすい。
何らか新しい局面へ移るとき、「ゼロから作り出す」 ( = フルスクラッチ) もしくは「既存のモノゴトを拡張する」の2通りに分けて考えるのが常である。

Intel 製 CPU の発展を遡ってみるとフルスクラッチ、つまり新築に該当する 32ビット CPU は 1980年代前半に発表されていた。あまり普及しなかった。その原因として「すでに浸透していた 16ビット CPU よりも性能が低い」点などが指摘されていた。

一方、1980年代後期、Intel の 32ビット CPU を搭載した PC が登場し普及した。この時採用された CPU は 80386。略して i386 や 386 と記されることもある。80386 は昨今の一般的な PC に搭載されている Core i7 や Pentium の源流である。
既存の 16ビット CPU で採用されていた命令体系をベースに追加、拡張が施され、互換性が保たれた。それまでの16ビット CPU を増改築したと言えよう。

普及する・しないを左右するのは、コストや性能といった観点だけではない。ユーザ側から観て、使い勝手の良し悪しも重要だ。
フルスクラッチの場合、既存のモノゴトに頼ることはできない。ユーザ側からすれば、ある程度状況が揃うまで待つことになる。
一方、従来からの互換性が保たれるとすれば、状況が揃うまで待つ必要はない。具体的に言えば、32ビット版のアプリが出揃うまで待つのではなく、出揃うまでの間は既存の16ビット版のアプリを使う選択肢も残されていた。
フルスクラッチの場合、以前と異なる点に直面する。過去記事アリさん100匹を観て想う-1、にて
「急激な変化だとついていけないと感じてしまう」と述べた。個人差はあれど新しいモノゴトに馴染むまで時間を要したり、全く馴染めないヒトも出てくる・・・

32ビット化された CPU が浸透した頃、次世代の64ビット CPU への模索が始まっていた。
Intel は 64ビットの命令体系として「IA-64」と呼ばれる命令体系を発表した。こちらはフルスクラッチ、つまり新築なので従来からの「IA-32」と互換性はない。
2014年 1月25日掲載分の記事で触れた Itanium シリーズが現時点で「IA-64」命令体系を実行できる CPU である。Itanium シリーズはデータセンターや大規模な科学技術計算用に向けた設計となっており、一般的な職場や家庭で使うような PC には搭載されていない。

「IA-64」を新築と例えるならば、Intel 製の CPU の互換プロセッサを作ってきた ( 競合プロセッサメーカー ) AMD から発表された「AMD64」と呼ばれる命令体系が「IA-32」に対する増改築と言える。「AMD64」は既存の 32ビット CPU で採用されていた命令体系をベースに追加、拡張が施され、扱えるレジスタ数などが強化された。もちろん互換性も考慮されている。
互換性うんぬんという表現では判り辛いかもしれない。かつて、Windows 95 など 32ビット版の OS で 16ビット CPU 用のアプリが動いたように、「64ビット版の OS でも従来からの 32ビット用アプリを動かすことができる」点は互換性が保たれていると考えてよい。
従来からの互換性が保たれる点は利用者にとって有利なだけでなく、アプリを組む側にとっても負担が少ない。それを象徴するように、OS を配給する Microsoft は Intel の「IA-64」ではなく、AMD の 「AMD64」を支持した、というよりも、積極的ではなかった。64ビット版の Windows XP が登場した際、「IA-64」用、つまり Itanium 用が流通していたが、Windows Vista 以降 Itanium シリーズはサポート外とされた。

結局、Intel は AMD が先に発表した「AMD64」の互換という後追いのような形で「EM64T」を発表した。80386 の流れをくむ 既存の32ビット CPU に「EM64T」命令セットを追加する形で 64ビット CPU への拡張が施された。2006年頃に登場したCore 2 Quad や Core 2 Duo の登場を境に「EM64T」は「Intel64」と呼ばれるようになった。

細かく見てゆくと「AMD64」と「EM64T」「Intel64」は若干の違いがある。これを書いた時点では「AMD64」や「Intel64」の総称として「x64」という呼び方が使われている。

※ Itanium にはエミュレーションモードが装備されており、従来の32ビット CPU 用のコードを実行することもできる。しかし、エミュレーションゆえの遅さは否めない。

SSE 命令を使う前に
「速度向上を目指し、データ転送部分を加工してゆく」に向けたお話に戻ります。

古典的な メモリ - メモリ間の転送とSSE2 を用いた場合を比べたいワケです。
後で出てくるSSE2 を使う場合に頭に入れておきたい点があるので先に述べておきます。
コピー元とコピー先、転送元と転送先を16 の倍数に整えておく。

Intel 製 CPU で SSE2 命令を用いるとして、XMM レジスタへのレジスタに読み込み( = ロード)、XMM レジスタ からメモリへの書き込み ( = ストア) を行う際
「アドレスは 16 バイトにアライメントが合っていなければなりません。」
等の注意事項がある。
※ アライメントをアラインメントと記す場合もある。
※ ほかの ( Intel 製 ではない ) プロセッサでも、特定の数の倍数でアクセスせねばならないモノもある。
※ AVX やそれ以降の拡張された機能では32バイトに合わせる

アドレスとは番地とも呼ぶが、データを読み出す場所、書き込む場所を指す。C/C++ 言語でいえばポインタの値、配列の場合はインデックス数を足した値が16 の倍数であるか否かということだ。
16 の倍数でないアドレスにあるデータをXMM レジスタにロードもしくはストアした場合は致命的なエラーとなって、アプリの実行が中断されてしまう。異常終了 ( ABnormal END ) は避けたいものだ・・・

「SSE や AVX など後から拡張された読み書き命令を使いたい場合、全て16 の倍数や32 の倍数に揃える必要があるのか???」
たしかに手間がかかりそう。しかし、それを避ける方法もある。
C/C++ 言語で書くとしてコンパイラ組み込み関数を用いるならば、SSE2 レジスタへの整数値ロードとストアは
_mm_load_si128
_mm_store_si128を用いる。これは「16 バイトにアライメント ~~」が必要な命令。
ところで、これらの関数名を見て戸惑ってしまうのも然り。組み込み関数の名前を分けて考えると理解しやすい。
接頭の「_mm」は SSE や MMXを使う際にお決まり、接尾の「_si128」はデータ型、この例では128bit幅の整数値データを扱う意味。接頭・接尾の語を外せば「_load」と「_store」が残る。
※ 実際には 128ビット幅の整数値の演算よりも、64ビット幅の整数値2個分、32ビット幅の整数値 4個分の演算を同時に行うためのロードやその結果をストアする。
それらの関数名に対し、「U」もしくは「u」が付いている命令が存在する。
_mm_loadu_si128
_mm_storeu_si128
先に挙げた2つの関数名と似ている。



よく比べてみよう。接尾の「_si128」の前に「u」が付いている点に気がついただろうか。



「_load」と「_store」の後ろに「u」を付けた組み込み関数を用いれば「~~ アライメントが16 バイト ~~」に気を配らなくとも良い。
冒頭で紹介した、IA-32 インテル アーキテクチャソフトウェア・デベロッパーズ・マニュアルが手元にあるならば 中巻Aの命令リファレンスより MOVDQA と MOVDQU の項目を参照。



「~~ アライメントが16 バイトに合っていなくても、一般保護例外(#GP)は発生しない。」と記載されている。
ならば、「ロード命令は全て _mm_loadu_si128 を使うと良いのでは!?」
との声も聞こえてきそう。そのご指摘はごもっとも。
SSE2 が搭載された当初、_mm_loadu_si128 など、後ろに「u」を付けた組み込み関数は遅いとされていた。それを補うように、SSE2 から SSE3 へ拡張される際、_mm_lddqu_si128 が追加された。これにより、「ほとんどの場合 movdqu よりもパフォーマンスが向上します」と説明されている。
筆者がこれを書いている時点での環境は「第 3世代 Intel Coreプロセッサー」( 通称 Ivy Bridge )。その環境において「u」を付けた組み込み関数で極端な速度低下はみられない。数年前より、この速度低下は改善されたようである。
「全て _mm_loadu_si128 を使うと良いのでは!?」と問われたのがSSE2 が搭載されて間もない頃であれば、答えはノー。しかし、速度低下というデメリットが改善された現時点では否定する理由はない。

車輪の再発明 (6)で VirtualAlloc 関数や HeapAlloc 関数を用いてメモリ空間を確保する例を挙げた。
VirtualAlloc 関数を用いた際に得られるアドレスは 64KB 、つまり 65536 バイトの倍数。2の16乗、2掛ける2掛ける2掛ける ~~ 掛ける2を 16回繰り返せば 65536。
16 は2の4乗、「2掛ける ~~」を 4回。VirtualAlloc 関数の仕組みとして 64KB 境界へ丸められるとされていることからアドレスを16で割って余りはゼロ。よって、「16 バイトにアライメント ~~」の条件を満たす。
一方、HeapAlloc 関数や他の関数ではアライメントを意識したメモリ割り当ては難しい。
※ Visual Studio ならば 2005 以降のバージョンで対応した _aligned_malloc 関数、ほかの開発環境では _mm_malloc 関数などを用いて特定のバイト数に整ったアドレスを割り当てることも可能。

アライメントや境界うんぬんの部分を自前でなんとかしてみよう。車輪の再発明 (6)で取り上げたコードの続きとして進めてゆく。

まず、16 バイトに合わせた ( 修正した ) 値を保持するための修正。
LPVOID lpTemp[2] = { NULL , NULL };

となっていた部分を
LPVOID lpTemp[4] = { NULL , NULL , NULL , NULL};

と変更する。lpTemp[0] と lpTemp[1] には VirtualAlloc 関数 や HeapAlloc 関数で確保したポインタ値、すなわちアドレスが格納されるようになっていた。lpTemp[0] と lpTemp[1] の値を16 バイトの倍数に合わせ、その値を lpTemp[2] と lpTemp[3] に格納する。
「16 バイトの倍数に合わせた値をlpTemp[0] と lpTemp[1] に直接格納しても良いのでは!?」と考えるヒトがいるかもしれない。lpTemp[0] と lpTemp[1] の値を変更しない・温存する理由は、VirtualAlloc に対する解放として VirtualFree 関数を用いるが、VirtualAlloc で得られた値と異なる値を渡すことを防ぐためである。
詳しくはMSDN ライブラリ、VirtualFree 関数の説明をご覧いただくとして、その説明の中に
「~~ this parameter must be the base address returned by the VirtualAlloc function when the region of pages is reserved.」
「~~ 領域を予約したときに VirtualAlloc 関数が返したベースアドレスを指定しなければなりません。」
といった文言がある。

16バイトに揃えるために
#if defined(WIN64) || defined(_WIN64)
#define BFMASK_PTRALIGN 0xffffffff00000000
#else
#define BFMASK_PTRALIGN 0x00000000
#endif // defined(WIN64) || defined(_WIN64)

#define MACRO_ALIGN_PTR(TP,PD,PS,OFFSET) {\
UINT_PTR uPtr = (UINT_PTR)(PS);\
if (uPtr & (OFFSET-1) ){uPtr += OFFSET;uPtr &= (BFMASK_PTRALIGN | (0xffffffff - (OFFSET-1) ) );}\
PD=(TP)(uPtr);}

この2つのマクロ定義を追加。追加する場所は冒頭、
#define MAX_LOADSTRING 100
の後あたりに追加するのが良いだろう。#define ディレクティブが複数行に連なるとき、末端の¥ ( 半角の円記号 ) を忘れないように注意。

このマクロ使い方の例は
MACRO_ALIGN_PTR(LPVOID , lpTemp[2] , lpTemp[0] , 16);

のようになる。追加する場所は VirtualAlloc 関数 や HeapAlloc 関数 が成功した後 ~ 初回の timeGetTime 関数を呼び出す前周りが良いだろう。

マクロが展開されて以下のように変換される。
{
    UINT_PTR uPtr = (UINT_PTR)(lpTemp[0]);
    if (uPtr & (16 - 1) ){
        uPtr += 16;
        uPtr &= (BFMASK_PTRALIGN | (0xffffffff - (16 - 1) ) );
    }
     lpTemp[2] = (LPVOID) ( uPtr );
}

この部分で行っているのは、
・ポインタ値が16の倍数かどうか比べる。
・16の倍数でないならば、16を足し、16で割った余りを取り除く。
uPtr +=uPtr &=の順序を逆にしたほうが伝わりやすいのかも。
今回は使う箇所が2箇所と少ないためマクロ定義を選んだ。使う箇所が多い場合はひとつの関数として設けたほうが楽。

割り当てられたアドレスが 39 だったとしよう。39は16の倍数ではない。これに近い16の倍数は 32 や 48。望ましいアドレスの値は 48。
まず uPtr += の部分で 39 に 16を足し、55となる。 uPtr &= の部分で 55 を 16で割った余り を取り除き 48となる。

55 は 日常生活で普通につかう 10進数。32 ビット幅の16進数で表現すれば0x00000037。
これを AND演算 を用いて32 ビット幅の16進数 0xfffffff0 でマスク、つまり、ふるいにかけることで16で割った余りを取り除ける。結果は16進数 で0x00000030となり 10進数 で表現すると 48。

なぜ、この方法で16で割った余りを取り除くことができるのか解せないヒトもいるだろう。
「0」と「1」の組み合わせによるAND演算を図で現すと以下のようになる。


1段目はもとの値、2段目がマスク、ふるい、3段目がAND演算した結果。
「0」を「いいえ」、「1」を「はい」と置き換えると理解しやすいかも。2つとも「はい」なら結果も「はい」。どちらか一方、もしくは両方「いいえ」ならば結果も「いいえ」つまり「0」となる
(16 - 1)を2進数「0」と「1」の組み合わせで表現すれば1111。8+4+2+1。16進数では 0xf。
AND演算でマスクした値の右端を注目、10進数 で15、16進数で f にあたる部分がゼロ。
もとになる値、2進数で下4桁分が 0 から 15 のどれであろうとマスクする値の下4桁分が 0なので、得られる結果も2進数で下4桁分は 0となる。
この手法は16 が2の4乗、2掛ける2掛ける2掛ける2であることから成り立っている。ほかにも 32 や 64 や 4096 といった2のN乗に該当する数値の場合に使える。
丁寧に進めたいならば、2のN乗に該当しない数値でも対応できるように16で割った余りを引き、その値に16を足すようなコードを書くべきだろう。なお、ここは速さを求める部分ではない。過去記事除算が遅い遅くない除算で取り上げたように、「遅くなる要因をなるべく排除したい」という観点から割り算以外の方法も選択しうるという例だ。常日頃それに適したほかの手段が存在するか否かを探すことは後々の糧となる。

BFMASK_PTRALIGN は 64ビットを対象とした場合、64ビット幅の上位32ビット分の情報を損失しないためのマスク。

ほか、UINT_PTR の部分に違和感を感じるヒトがいるかもしれない。
UINT は 「符号なし整数」を意味する。C/C++ 言語の入門書等に載っている unsigned int と同意。「_PTR」の付く整数型は 「32 ビット版の Windows と 64 ビット版の Windows の両方のポインタに合わせてサイズが変更される整数型」との説明がある。「_PTR」の付く整数型を使わずに作るとなると、プラットフォームが変わる毎に UINT や unsigned __int64 へと修正する手間が増える。

ポインタ値を符号なし整数として扱い演算し、演算結果をポインタへ戻す。丁寧に進めるならば、ポインタを整数値に変換する部分などは型変換として、reinterpret_cast を扱うべきなのだろう。
型変換について載せている解説系のサイト等を眺めると「キャストの危険性」を唱えている。ハードウェアを意識しない言語で組むヒトほど顕著なのだそうだ。
ここでは一般的な PC に搭載されている Intel 製 CPU を前提にしているのに加え、ビット幅が変わる変換ではない。よって、「キャストの危険性」は気にせずに書いてみた。
アセンブリ言語を扱うようなレベル、機械側に寄った見方で言ってしまえば、ポインタも「_PTR」の付く整数型も同じ数値。そもそも、ポインタや整数値といった概念は人間が判断しやすいように分けて使っているのであって、CPU 自身には判断できないものだ。
とはいえ、「キャストの危険性」を否定する気はない。実際にビット幅が異なる型変換はトラブルのもと。小さい型への切り詰めは情報が失われる恐れがあるので注意が必要だ。
情報が失われるという表現では飲み込み辛いヒトもいるだろう。
旅館を例にあげてみよう。以前からの本館に加え、最近新館(2番目の館)を増設したとする。
「この荷物を新館の3階の2号室に届けて」と伝えるべきところを、
「この荷物を3階の2号室に届けて」と伝えたのでは誤が生じることだろう。ここで情報が失われたのは、「本館なのか新館なのか?」という点。
たしかに、人間であれば前後の文脈や雰囲気から、届ける先が本館なのか新館なのかを判断できるかもしれない。しかし、機械は生真面目ゆえに融通が効かない・・・

これで一安心・・・ではなく・・・ほかにも重要な修正箇所があるので忘れないようにしたい。
16の倍数にポインタ、アドレスの値を修正するということは、16バイト未満の範囲で 大きくなることが想定される。よって、メモリ空間を確保する際に領域のサイズ ( 割り当てたいバイト数 ) に余白分を追加する修正が必要。
もしくは、修正済みアドレスから修正前のアドレス値を引き、その値を有効領域と変更する案もあるが面倒。

車輪の再発明 (6)で載せたコードではメモリ空間を確保する際に、余白なしでサイズ指定した。
修正が必要な箇所は
if ((lpTemp[0] = ::VirtualAlloc(NULL , __SIZE_DUPLICATE , MEM_COMMIT , PAGE_READWRITE) ) == NULL){
もしくは
if ((lpTemp[0] = (LPVOID) HeapAlloc(hHeap , HEAP_ZERO_MEMORY , __SIZE_DUPLICATE) ) != NULL){

のような部分。
最大で15 バイト分はみ出す可能性がある。余白として増やすべきサイズは 16。安全のため 32でも良いだろう。後で触れることに絡み余白を 256 と設定してみる。~~ Alloc 関数の引数
, __SIZE_DUPLICATE
となっている部分を
, (__SIZE_DUPLICATE + 256)
のように修正することで、確保したメモリ領域をはみ出すようなアスセスを防げる。

さてさて、 CopyMemory 関数 を書き換えてみる・・・と続けたいところですが・・・

長くなりましたので続きはまた後日・・・
スポンサーサイト

本日も最後までご覧いただきありがとうございます。

「つまらなかった」「判り辛った」という方もご遠慮なくコメント欄へどうぞ

テーマ : プログラミング
ジャンル : コンピュータ

[未掲載分] 車輪の再発明 (7)

元来、好ましい意味で用いられない「車輪の再発明」。



筆者は、そのような行為を肯定的に捉えている。
なぜならば、新しい技術や文化が芽生えるきっかけは、一見無駄と思われることへの挑戦である・・・


お知らせ
活動休止にともない、この記事を事前に予約投稿してあります。
トップ記事の固定を目的としています


この題目は2012年暮れ頃に掲載しようと下書きし、諸事情により掲載却下とした分です。
前話、車輪の再発明 (6)にひきつづき、利用環境を OS はWindows , CPU は Intel 製、いわゆるウィンテル機を前提に綴ります。

アセンブリファイルを出力

まずアセンブリ言語ファイルの出力について述べておきたい。
アセンブリ言語ファイルを出力する目的は、C/C++ 言語で書いたコードがどのように変換されたかを確認するためである。

Visual Studio の利用を前提に話を進める。初期状態ではアセンブリ言語コードや機械語のファイルは出力されないように設定されている。
プロジェクトのプロパティで簡単に変更できる。
すでに車輪の再発明 (5)で触れている通り、プロジェクトのプロパティを開くには Visual Studio の左側ペイン「ソリューションエクスプローラ」から、プロジェクトファイルのアイコンを右クリック。
※ キーボード操作ならば、[Alt]ボタンを押しながら[F7]を押す。



ダイアログボックス左側ペインのツリービュー、「構成プロパティ」の「C/C++」を選択。
[出力ファイル]の中に、[アセンブリの出力]という項目がある。初期状態では「なし」、ゆえに、出力されない状態に設定されている。



[アセンブリの出力]のドロップダウンリスト ( 右隅にある下向き三角 ) をクリックして
「アセンブリコード」「コンピュータ語コード」「ソースコード」を選択。
これで、「もとのC/C++ 言語コード」、「機械語」「ニーモニックコード」が出力されるようになる。

初期状態でソリューションフォルダー内に Release や Debug というフォルダーが作成されるハズ。それらの中に「.cod」という拡張子が付いたファイルが作成される。
拡張子が「.cod」となっているファイルを開いてみると、もとのソースコード、それに続き 16進数、アセンブリ言語のコードが並んでいる。



16進数の部分がいわゆる「機械語」「マシン語」と呼ばれる部分で、CPU にとって直接理解できる状態。
その右側に並ぶ、call や mov や add の部分は「アセンブラコード」や「ニーモニックコード」と呼ばれる。16進数で羅列される数値だけでは人間にとって判り難いので、それを補う役割がある。例えば加算なら ADD , 乗算なら MUL などなど。

古くから PC に触れているヒトにとってはアセンブリ言語しか選択肢がなかった頃もある。
やがて人間にとって判り易い言語で書いて、翻訳したアプリを実行するようになり、アセンブリ言語を主軸に組むことは減った。
今後、プログラミングを覚えてゆこうとしているヒトにアセンブリ言語や機械語から学び始めるのはお勧めできない。
アセンブリ言語で組むとなると、ハードウェアを直接叩くことになる。ちょっとしたミスで致命的な状況、ブルースクリーンや暴走へとつながる。たとえ熟練のヒトであってもちょっとしたミスは避けられない。
さらに、Visual Studio で 64ビット用のアプリ作成する際にはインラインアセンブラが利用できない。

C/C++ 言語や他のプログラミング言語から機械語に翻訳された過程やプログラミングされたコードの流れを把握することは有益である。アセンブリ言語を読めるようになれば、翻訳、変換の過程から、もっと良いコードを探ることやハードウェアへの理解が深まることだろう。慣れてくれば、トラブルシューティングも迅速に行える。

この題目のきっかけとなったブロガーさんにかぎらず、ハードウェアを深く知らずともアプリを作成することは可能だ。
むしろ、高水準な言語、お手軽な言語でコード入力等に要する時間をいかに削減するのが時代の流れである。
アプリを作れるようになると「どうすれば高速に動くのか」「ハードウェアの性能を引き出したい」など興味が沸くも自然な流れである。しかし、ハードウェアに対する理解が浅い状態で「高速に~」を試みるのは巨大な風車に立ち向かうドンキホーテのようなものだ・・・

実行ファイルの切り替え

標準的なモノと少々細工を加えたモノ、さらに深くチューニングしたモノ、それぞれを比べたい。何を比べようとしているかについては、過去記事車輪の再発明 (2)で述べた通り。
複数のモノを比べる際、「対象を手軽に切り替えたい。」と考えるのは自然なコト。
ここで比較対象とは実行ファイルを指す。

実行ファイルを切り替えようとして、簡単に思いつく方法は、
・プロジェクトのコピーを複数用意して、それぞれに別々のコードを書く
・単一の実行ファイル内に「開始時刻の取得 ~ 測りたい処理 ~ 終了時刻の取得」を複数設ける
・「測りたい処理」部分を関数ポインタで動的に変更する
・条件つきコンパイルで複数の実行ファイルを作成し、それぞれを実行して比較。
などだろうか。

まず、最初の「複数のプロジェクトファイル~~」は即却下。理由としては管理の手間が増え、修正漏れなどのミスが生じ易い。

同様に
・単一の実行ファイルで「測りたい処理」を作る度にビルドし実行、計測してゆく
といった力任せな手法も避けたいものだ。修正漏れなどのミスが生じ易いだけでなく、ビルドする度に多くのメモリを費やしたり、メモリの断片化が起こる。安定した状態での比較は難しくなる。
安定した条件で比較することを考えると、PC を再起動し数分間経った後の「PC がヒマな状態」で計測するのがベスト。ディスクのアクセスLEDやネットアクセスを示すLEDの点等が著しい間はヒマな状態とはいえない。起動した後何も作業していない状況ならば未使用のメモリ空間も多く残っているハズ・・・

今回は2番目の、単一の実行ファイル内に「開始時刻の取得 ~ 測りたい処理 ~ 終了時刻の取得」を複数設ける案が無難な選択肢となるだろう。
CPU の キャッシュメモリ容量に左右されてしまう内容であれば要注意だが、今回はキャッシュに収まりきれない容量なので配慮は不要。
※ キャッシュ容量に左右されないようにするには、キャッシュの内容をクリアするコードを追加すれば対処できる

3番目の関数ポインタうんぬんに関して言えば、検索サイト等からこの記事にたどりつくヒトを想定するならば敷居が高い話となる。よって今回は却下。

最後の「複数の実行ファイル」や「条件つきコンパイル~」は不慣れなヒトのために載せておきたい。
まず、複数の実行ファイルを作成するにはバッチビルドを行うと楽。
同じプロジェクトで構成を少し変えて実行するには便利である。もちろん、複数のプロジェクトを一括ビルドする際にもバッチビルドが活きてくる。
プロジェクトの設定変更については、過去記事車輪の再発明 (4)で取り上げた。

バッチビルドを行うにはいくつか方法がある。今までの流れに従い Visual Studio の利用を前提に話を進める。
メニュバーの[ビルド]をクリックし、表示されたメニューの中から[バッチビルド]を選択。もしくは、キーボードの[Alt]ボタンを押しながら[B]ボタンを押し、[T]ボタンを押す。



ここで表示されたダイアログボックスのリストビューで構成を確認できる。



リストビューのカラムヘッダは横方向に「プロジェクト」「構成」「プラット・・・」と並んでいる。
「構成」の行、縦方向に「Debug」「Relase」と並んでいる。
過去記事車輪の再発明 (4)で触れたように、「Debug」と「Relase」の違いは「リハーサル用」と「本番用」のように考えると判り易いだろう。

「プラット・・・」の行は過去記事車輪の再発明 (4)で64ビット向けのアプリを作れるように設定変更したので、Win32 , x64 と交互に並んでいるハズ。

ここに、比較用にパターン2を追加してみる。
新しい構成を追加するには、ひとつ前の図、メニュバーの[ビルド]をクリックしてメニューがポップアップされた様子をご覧いただくと判り易い。メニューの項目「バッチビルド」のひとつ下に表示されている「構成マネージャ」を選択。
もしくは、キーボードの[Alt]ボタンを押しながら[B]ボタンを押し、[O]ボタンを押す。



ほかにも、Visual Studio のメニューバー下、ソリューション構成のドロップダウンリストをクリックして「構成マネージャ」を選択する方法もある。

構成マネージャのダイアログボックス左側、



「アクティブソリューション構成」のドロップダウンリストから「<新規作成>」をクリック。



「新しいソリューション構成」のダイアログ、[ 名前 (N) ]のエディットボックスに追加したい件の名称を入力する。
ここでは、Release 版、第2パターンということで「Release_p2」と設定した。さらに第3番目のパターンを追加するならば「Release_p3」などなど・・・



新しい構成は Release 版をベースに作成したい。[ 設定のコピー元 ]のドロップダウンリストから「Release」を選択する。
このドロップダウンリストが「空」のままであったり、「Debug」が選択されているようなコトが無いように確認・・・

順調に進めば、バッチビルドのダイアログ、リストビューに第2番目のパターンの構成が追加される。



この手順で、第3、第4、第5と比較したい構成を追加してゆく・・・

条件つきコンパイル

C/C++ 言語にかぎらず、「条件つきコンパイル」という仕組みがある。
おおまかな部分はほぼ共通、一部は個別の状況に合わせて差し替えたいといった場合に好都合。
条件によって翻訳の原文を切り替えることができるので、一つのソースコードを基に複数の異なる条件で共有したい場合などに便利。

C/C++ 言語においてはプリプロセッサディレクティブ、#define ディレクティブ#if ディレクティブ ~~ #else ディレクティブ ~~ #endif ディレクティブを組み合わせると手軽に切り替えることが可能。

#define ディレクティブには複数の使い方があり、主な使い方を2通り取り上げる。「#define 定数名」と「#define 定数名 数値や文字列」では意味合いが異なる。

「#define ディレクティブ」の前者の例としては
#ifdef _DEBUG
// Debug 版で用いるコード
#endif //_DEBUG

といったところが判りやすい。「_DEBUG」 というマクロが定義されていれば「#ifdef」 と「#endif」で挟まれた部分が有効になる。上の例は「もし ○○ ならば△△」の意味合いで「#ifdef] と「#endif」で挟んでいる形。
「もし ○○ ならば△△それ以外は■■」のように使いたいならば

#ifdef _DEBUG
// Debug 版で用いるコード
#else
// Debug 版以外で用いるコード
#endif //_DEBUG

のように書く。違いは「#else」が挿入されている点。これにより、「_DEBUG」が定義されていないならば 「#else」から「#endif」の間のコードが有効になる。
#ifdef 定数名#if defined(定数名) と書いても同じ意味となる。
例えば、64ビット版のアプリと32ビット版でアプリ毎に別々のコードを適用したいならば
#if defined(WIN64) || defined(_WIN64)
// 64ビット版向けのコード
#else
// 32ビット版向けのコード
#endif // defined(WIN64) || defined(_WIN64)

といった感じで使うのも良いだろう・・・

※ Intel 製 CPU 、OS は Windows という状況において、64ビット版の OS では 64ビット版向けのコードも32ビット版向けのコードも実行可能。32ビット版の OS では32ビット版向けのコードと16ビット版向けのコードが実行可能であるが、32ビット版の OS では 64ビット版向けのコードは実行できない。

「#define ディレクティブ」の後者の例としては、定数名に数値が結びつくパターン。
参考例としては、ヘッダーファイル「targetver.h」もしくは「stdafx.h」の中に記されている「#define WINVER 0x0600」が判り易いのではないだろうか。「最低限必要なプラットフォームが Windows Vista であることを指定~~」と注釈が付いていることから、おおよその意味を察することができるハズ。
実は、Windows Vista より前、Windows Vista 以降では挙動が異なる例がいくつかある。Windows Vista より前ではサポートされていない機能もある。
アプリの実行条件・対象を明確に指定することで、サポートされていない機能を使おうとして生じるエラーなど、実行時のトラブルを未然に防ぐのに役立つ。

最低限必要なプラットフォームを Windows Vista と設定して #if ディレクティブを使うならば
#if defined(WINVER) && (WINVER >= 0x0600)
// Windows Vista 以降向けのコード
#endif // defined(WINVER) && (WINVER >= 0x0600)

のようになるだろう。さらに条件の一致と不一致で分けるならば #else ディレクティブ を間に挿し、

#if defined(WINVER) && (WINVER >= 0x0600)
// Windows Vista 以降向けのコード
#else
// Windows Vista より前向けのコード
#endif // defined(WINVER) && ( WINVER >= 0x0600 )

と使うこともできる。
ところで、WINVER マクロで有効な値、すなわちWindows のバージョンを示す数値として
Windows 2000 は 0x0500 、XP ならば 0x0501 、Windows7 は 0x0601 、Windows8 は 0x0602 などが定義されている。
Windows のバージョンごとに細分するならば
#if defined(WINVER) && (WINVER >= 0x0601)
// Windows 7 以降 向けのコード
#elif defined(WINVER) && (WINVER >= 0x0600)
// Windows Vista 以降 向けのコード
#elif defined(WINVER) && (WINVER >= 0x0500)
// Windows 2000 や XP 向けのコード
#else
// どの条件にも 一致しない Windows 向けのコード
#endif // defined(WINVER) && (WINVER >= 0x0601)

といった使い方もできる。実際は新規に何かを作成する場合には将来に向けたモノを作るべきであり、古い OS やサポート期間が終了した OS を特定の対象にすることは稀有だろう・・・

#define ディレクティブ#if ディレクティブ ~~ #else ディレクティブ ~~ #endif ディレクティブに慣れてしまうほど使いたくなるものだ。しかし、用い過ぎると読み易さが損なわれる。
ちなみに筆者は「#endif」 の直後にコメントを付けるよう心がけている。「#if 」と「#endif」は対にして、ワンセットにすることになっている。どちらかが欠けてしまうとエラーで翻訳できない。重複して使うことは可能だが、時には入れ子が深くなり「この#endifはどの#ifに対するモノ???」 といった状況にもなりかねない・・・

ここで誤解が生じないように補足。
ここまでの話は「#define ディレクティブや #if ディレクティブなどの使い方」がキモであり、プラットフォームをリアルタイム判定しているのではない。
アプリをビルドする段階では「どのバージョンのWindows 上でアプリが実行されるのか」を知ることができない。
利用環境は十人十色、つまり、アプリが実行されるまでどのバージョンのプラットフォームなのかは不明である。
その辺について知りたいヒトもいることだろう。
アプリの中にGetVersionEx 関数 や VerifyVersionInfo 関数などを設けることで現在のプラットフォーム・Windows のバージョンが数値として得られる。
その数値をもとに、最低限必要なバージョン番号およびそれより大きいバージョン番号であるかどうかをチェック。もし、バージョンが低いならばメッセージボックスを表示してアプリを終了する。といった流れにすれば使う側から観て親切。
バージョンチェックを追加する場所はウィンドウフォームを表示する前、できれば WinMain 関数の冒頭で行うのが良いだろう。

※ GetVersionEx 関数 や VerifyVersionInfo 関数 を検索するためのリンクを貼っておきます。詳しく知りたいヒトはMSDN ライブラリ、できれば日本語版だけでなく原文にも目を通せば情報の見落としが減ることでしょう。
GetVersionEx関数を検索するためのリンク
VerifyVersionInfo関数を検索するためのリンク

アプリ毎に定数を設定

しばしば机上の論と実情は異なる。
教科書的に#define ディレクティブでの切り分けとなると、実際のところパターン毎に設定を書き換えるので微妙に不便。
ちょっとしたミスが含まれていた場合は手直ししたくなるものだ。その際、パターンの数だけ「#define ○○」の部分を書き直し全ての構成をビルドし直すことになり面倒。

複数ビルドの手間を考えると、プロジェクトの構成ごとにプリプロセッサ設定を変更するのが楽である。
例として、「__MYAPP_LEVEL」という定義名を用意してパターン毎に違う数値を割り当ててみる。
※ 定義名はこの例の通りでなくとも構わない。ただし31文字以内で収まるように注意すべきである。

プリプロセッサの定義を変更するには、プロジェクトのプロパティを開いて個別設定を行う。
プロジェクト毎の設定を変更する件は車輪の再発明 (4)で述べた。また、この記事の冒頭でアセンブリファイルを出力する件として触れている。

プロジェクトのプロパティを開いて個別設定を行うには、操作画面メニューの「プロジェクト」 -> 「プロパティ」とクリックするよりも、キーボードの[Alt]ボタンを押しながら[F7]を押すと楽。

ダイアログボックス左側ペインのツリービュー、「構成プロパティ」の「C/C++」を選択。
さらに ツリービューの[プリプロセッサ]を選択。



ダイアログ右側に「プリプロセッサの定義」という項目がある。ここに追加したい定数等を入力する。

車輪の再発明 (4)の例では64ビット版のアプリを作りたいので
「;WIN64;_WIN64; ~~ ;UNICODE」のを付け加えた。その後ろに
;__MYAPP_LEVEL=2
を付け加える。区切りのセミコロン「;」を忘れない、空白を入れないように注意。



「これは第2番目のパターンですよ」という意味だ。 同様に3番目のパターンのコードには ;__MYAPP_LEVEL=3 4番目のパターンのコードには ;__MYAPP_LEVEL=4 といった具合に変更を加えてゆく・・・

先に述べた#if ディレクティブ ~~ #else ディレクティブ ~~ #endif ディレクティブを使うとして、
ソースコードは以下のような感じ。
#if __MYAPP_LEVEL == 2
// 第2番目のパターンのコード
#elif __MYAPP_LEVEL == 3
// 第3番目のパターンのコード
#elif __MYAPP_LEVEL == 4
// 第4番目のパターンのコード
#else
// どの条件にも 一致しないコード
#endif //__MYAPP_LEVEL == 2

※ __MYAPP_LEVEL が定義されているか不確実な時は #if defined(__MYAPP_LEVEL) && (__MYAPP_LEVEL == 2) と書いたほうが丁寧。今回は確実に定義されているとして、省いています。

複数の実行ファイルを一箇所に作成

ここまで順調に進めば複数のアプリを作成できるハズ。

実行ファイルの名称が全て同じであり、別々のフォルダーに格納される。
単一の実行ファイルを作成している場合、不便に感じることは少ないことだろう。一方、複数のアプリを作成した場合、アプリを起動する度に別のフォルダーを開いたり、戻ったりの手間が増える。面倒という声が聞こえてきそうだ。
ならば、同じフォルダーに名称の異なる実行ファイルが作成されるように設定変更すれば手間が減る。

実行ファイルの出力先を変更する件については車輪の再発明 (4)の終盤で触れた。
プリプロセッサの定義を変更した際と同様、プロジェクトのプロパティを開いてプロジェクト毎に設定を変更する。



ダイアログボックス左側ペインのツリービュー、[リンカ]の[全般]を選択。
そして、右側にある[出力ファイル]という項目に注目。



初期状態で出力先は
$(OutDir)\$(ProjectName).exe
などと設定されているハズ。ここを変更する。

例えば、第2番目のパターンのコードを作成したいなら
$(SolutionDir)$(ProjectName)_p2.exe
と変更する。同様に3番目のパターンのコードならば
$(SolutionDir)$(ProjectName)_p3.exe
4番目のパターンのコードならば・・・と設定を変更してゆく。

バッチビルドのダイアログボックスで、ビルドしたい全てのパターンにチェックを入れ[ ビルド ]ボタンをクリックする。
一括ビルドが順調に完了すれば以下の画像のようになる。



同一フォルダー内に名称の異なる実行ファイルが作成されるハズ。
これで複数パターンの実行ファイルを比べるのが楽になりそうだ・・・

長くなりましたので続きはまた後日・・・

本日も最後までご覧いただきありがとうございます。

「つまらなかった」「判り辛った」という方もご遠慮なくコメント欄へどうぞ

テーマ : プログラミング
ジャンル : コンピュータ

[未掲載分] 車輪の再発明 (6)

元来、好ましい意味で用いられない「車輪の再発明」。



筆者は、そのような行為を肯定的に捉えている。
なぜならば、新しい技術や文化が芽生えるきっかけは、一見無駄と思われることへの挑戦である・・・


お知らせ
活動休止にともない、この記事を事前に予約投稿してあります。
トップ記事の固定を目的としています


この題目は2012年暮れ頃に掲載しようと下書きし、諸事情により掲載却下とした分です。
前話、車輪の再発明 (5)にひきつづき、利用環境を OS はWindows , CPU は Intel 製、いわゆるウィンテル機を前提に綴ります。

ここから筆者の雑感 ---

今回の内容もソースコードを載せ、解説へのリンクを張れば済みそうな話。
筆者が相談を受けた内容から、端折らずに載せた方が良いだろうと判断した。
質問される事は苦ではない。たいていは「説明書を読んだ?」で解決しそうなケースだが、稀に「たずねる側は筆者のことを家庭教師か何かと勘違いしているのではないか!?」と疑いたくなるケースさえある。

今回のようなテーマならば、C99 や( 2011年版の ) C11規格 といったC 言語仕様、MSDN ライブラリ、Visual Studio のヘルプ機能等から欲する情報にたどりつける。

能動的に「己から積極的に学ぶ」ことができるヒトがいる、一方、誰かに教えてもらう受動的な傾向のヒトもいる。何事も不慣れな段階においては他を頼りたくなるものだ。、己の力としたいならば己で探りだせるようになってほしい・・・

--- ここまで筆者の雑感

前回までで、アプリのフォームにボタンを着け、そのボタンをクリックすることで新しく設けた関数を呼び出せるようになっている。今回は、前回までの流れの続きとして、
・ メモリの確保
・+ 開始時刻
・++ 処理
・+ 終了時刻の取得
・+ 処理にかかった時間、開始時刻、終了時刻の差を表示
・ 確保したメモリを解放
と進む。

(3-1) 関数の戻り値を変更

関数の戻り値と表現すると堅苦しいかもしれない。おおざっぱに言えば、「作業は成功?それとも失敗?」を確認することだ。
前回までで作成した関数は、単純に成功を意味する 「NO_ERROR」を返すようになっている。状況に応じた返答ができるように変更する。

LONG execBench(HWND hOwner)
{
    LONG lRet = S_FALSE;
    // ここに(3-2) 以降のコードを追加する
    return lRet;
}

のような感じ。この関数を呼び出した場合、失敗を意味する「S_FALSE」が戻る。

変数 lRet には、エラーコードが入る。途中の段階で何らかの問題が生じた場合、エラーコードを格納。
処理が全て成功した段階で 変数 lRet に「S_OK」もしくは「NO_ERROR」を代入する。

「S_OK」と「NO_ERROR」は同じ 0 を意味する。「S_FALSE」や「S_OK」は HRESULT型の戻り値として使われる。
Windows 上で動作するアプリを作るうえで HRESULT型 が頻繁に登場する。
HRESULT の定義は、ヘッダーファイル「winnt.h」の中で行われており、
typedef LONG HRESULT;
となっている。実質的には long を指している。なぜならば、大文字で記される LONG 型 も
typedef long LONG;
と定義されていることから、LONG は long の別名と解釈できる。
筆者は LONG 型のまま話を進めるが、気になるヒトは LONG の部分を HRESULT に置き換えると良いだろう・・・


※ これは2013年2月18日に掲載した記事の画像です

予め述べておきます。
C/C++ 言語において、{} で囲まれた部分はブロックと呼ばれている。
一般的に数学などでは、
e - [d -{c - (a + b)}] のように、深さによって囲む記号・符号が変わる。
一方、C/C++ 言語 では、
e - {d -{c - {a + b}}} のように、深さに関係なく同じ波括弧の記号で囲む。

この後の内容は、入れ子(ネスト)、つまり、{}の内側に{}が入る状態が深めのまま載せてあります。出口を揃える意図。

C/C++ 言語 でも他の言語と同様、無条件分岐、つまり、任意の場所に直接ジャンプすることを意味する goto が用意されている。
教本等では goto はなるべく使わないなどと記されている。中には goto を 禁止、厳禁と記しているモノまである。
エドガーダイクストラ (Edsger W. Dijkstra) によって「構造化プログラミング」が唱えられた頃から、無条件分岐等を使わないスタイルが浸透してきた。
※ 厳密に言えば、「構造化プログラミング」と「無条件分岐の排除」は異なる。

C/C++ 言語 に不慣れな段階ではそれに従うのが良いだろう。なぜならば、無条件分岐を多用することで、複数の出口を設けてしまうミスに繋がる可能性がある。
「入り口に対する出口が複数の場合、後日、メンテナンスの際に苦労する」というは、多くの先人達が経験してきたことだ。

とはいえ、あくまでも「goto を絶対使うな」というのではなく、「なるべく使わない」「できるだけ使わない」である。
何らかのプログラミング言語を習得していた場合、goto を使いたくなるのも当然である。
C/C++ 言語を前提に考えるならば、反復処理はwhile 文 for 文 などで代行、条件判断は、if 文の入れ子構造を少し深くしたり、switch文 を用いることで goto に頼らずに済む。
仮に goto を用いるとしても、「流れを遡るような分岐は使わない」「複数の出口を設けない」など、一定のコンセプトに基づいていれば問題も少く抑えられるのでは。

なお、人間が直感的に判断し易いネストの深さは3~4層くらいと言われている。幾重もの深さになってしまうと読みやすさが損なわれる。そのような際は、外部に別の関数を設けるなど工夫すれば読み易くなるハズ・・・

(3-2) 作業領域を確保

メモリを確保する大きさを定義する。この関数を通して
「メモリー間の転送速度を向上できるのか!?」
メモリ - メモリ間の転送速度を調べたいワケです。なぜ調べたいのかは、過去の記事車輪の再発明 (2)をご覧あれ。

16MB や 32MB くらいの領域 ( 区間の長さ )ではキャッシュが効いてしまい、欲する値とかけ離れるた結果となってしまう。そこで、キャッシュに乗り切らないようなサイズ 128MB もしくは 256 MB を定義する。

冒頭のほうに
#define __SIZE_DUPLICATE (1024*1024*128)

もしくは
#define __SIZE_DUPLICATE (1024*1024*256)

と定義を追加。
#define MAX_LOADSTRING 100
の後あたりに追加するのが良いだろう。コピー元とコピー先を2箇別々の空間として扱うので、定義した2倍サイズの空きメモリが必要。

メモリの確保や解放を行う関数

次に、メモリの確保、それと対になるようにメモリ解放の関数を追加する。
追加する位置は、execBench( 関数の LONG lRet = S_FALSE; と return lRet; の間。

    LPVOID lpTemp[2] = { NULL , NULL };

    if ((lpTemp[0] = ::VirtualAlloc(NULL , __SIZE_DUPLICATE , MEM_COMMIT , PAGE_READWRITE) ) == NULL){

        lRet = ERROR_OUTOFMEMORY;

    } else {

        if ((lpTemp[1] = ::VirtualAlloc(NULL , __SIZE_DUPLICATE , MEM_COMMIT , PAGE_READWRITE) ) == NULL){

            /* if (lpTemp[0]) */ ::VirtualFree(lpTemp[0] , 0 , MEM_RELEASE);

            lRet = ERROR_OUTOFMEMORY;

        } else {

            // ここに(3-3) 以降のコードを追加する

            /* if (lpTemp[1]) */ ::VirtualFree(lpTemp[1] , 0 , MEM_RELEASE);
            /* if (lpTemp[0]) */ ::VirtualFree(lpTemp[0] , 0 , MEM_RELEASE);

            lRet = S_OK;

        }

    }

古典的な教本に従えば、メモリの確保と解放はmalloc とfree。現状では、HeapAlloc と HeapFree の組み合わせ、もしくは new と delete を用いるのが一般的。

Windows に限って言えば、16ビット時代から残っている LocalAlloc や GlobalAlloc といった関数もある。32ビット OS が登場してからLocalAlloc や GlobalAlloc の利用は非推奨、HeapAlloc や VirtualAlloc への切り替えが唱えられてきた。
64ビット OS が登場した昨今、教本やサンプルで GlobalAlloc を軸に載せている例もある。クリップボードの操作やBSTR 系の文字列操作で GlobalAlloc が必要とされている。
なお、GlobalAlloc 関数で割り当てられる限界は256MB 程度。

ここでは、Windows に特化した話なので、VirtualAlloc と VirtualFree を用いたコードを載せた。
※ 正しくは、VirtualAlloc で確保した空間をメモリへ結びつけるには VirtualLock 関数を用いる。今回載せた分では抜けている。メモリ空間を Lock する際の副作用、弊害の話は長くなりそうなので省く・・・

Virtual ~~ を用いる理由は、巨大なメモリ空間を確保したいから。
今回試したい内容に関しては、小さな空間、つまり短距離間の動作速度を測っても意味が無い。
VirtualAlloc と比べ HeapAlloc は短距離向けのイメージ。実のところ、Virtual ~~ と Heap ~~を分ける明確な基準はない。個人的な感覚で言えば 500 KB 程度のメモリを確保するケースならば HeapAlloc で良いだろう。

アライメントの手間、つまり割り当てられるメモリのアドレスと実際に使うアドレスの微調整が必要となるケースも出てくる。
その手間を考えると VirtualAlloc ならば、64KB ( 65536 バイト) の境目に揃うので楽。

ほかにも、HeapAlloc の前に HeapCreate が必要。もちろん、HeapCreate でヒープを作成した際は、対になる HeapDestroy 関数、ヒープの破棄も必要。HeapDestroy を付け忘れれば、メモリリークの要因となる。

GetProcessHeap 関数を用いて ヒープを作成を省くこともできる。その際「一般的なアプリのデフォルトスタックサイズが 1MB」という点に注意が必要。
※ 「スタックサイズが 1MB」というのは32ビット版のWindows が登場した頃からの情報。

しばし、char 変数名[256];などといったコードも見かける。
練習段階ではこのようなコードも参考になるだろう。筆者としては悪い習慣にならないように願うばかりである。
幸いなことに、Visual Studio でアプリ ( ウィンテル機向け ) を組むと、このようなコードも通る。エラーにならない。
文字変数の一文字あたりが1バイトであれ2バイトであれ、 変数名[256] は 「スタックサイズが 1MB」のおかげでエラー無しで通る。「スタックサイズが 1MB」 を認識した上で用いるのは結構だが、ほかの環境でのスタックサイズは???である・・・

不慣れなヒトにとって、「デフォルトスタックサイズ」 というのは判り難い???
「デフォルト」は初期状態。「スタックサイズ」は、アプリを表舞台と例えるならば、「舞台そで」「舞台裏」「楽屋」の広さといったところ。

スタックサイズが足りないならば、割り当ての変更は可能 。



Visual Studio を使っているならば、ツリービュー、[リンカ] -> [システム] と辿り、スタックやヒープの設定を変更できる。
「スタックやヒープの設定を変更しなければ動かないアプリ」を作ってしまうよりも、設定の変更せずとも動くようにアプリの構造を見直したいものだ・・・

古典的なメモリ割り当て系の関数と比べ、Windows で用いるメモリ割り当て系の関数は「割り当てたいサイズ」だけでなく「割り当ての方法」や「割り当てのタイプ」を指定する。VirtualAlloc 関数を呼び出す際のオプションは MEM_COMMIT とした。
MSDN ライブラリによれば
「メモリ内またはディスクのページングファイル内に ~~ 割り当てます。」
との記述がある。
筆者の環境 ( Windows7 64ビット版 ) では物理メモリ上に割り当てられた。ここで陥りがちなのは、自の環境で起きた事象は他の環境でも同様に起きると錯覚すること。常にメモリ上に割り当てられると想定すべきではない。結果は実行する環境に左右されるのだ。
PC に搭載されているメモリが少ない場合は、ディスクのページングファイル内、つまり仮想ファイル上に割り当てられることもある。
ファイル上に割り当てられてしまうと、物理メモリへの読み書きではなく、ディスクの読み書きへ置き換わる。
ちなみに、メモリとディスクとの速度差は10万倍以上とも言われている。

この題目のきっかけとなったブロガーさんは、Windows XP を利用しているとのこと。
32ビット版のWindows では利用できるメモリ容量との絡みでディスク上に割り当てられてしまう可能性もある。
「物理メモリ上に割り当て」が譲れない、MEM_PHYSICAL オプションを指定すると良いだろう。
32ビット版のOSでも AWE ( Address Windowing Extensions ) がサポートされている環境という条件を満たせば、より多くのメモリ空間を扱える。とはいえ、今後は AWE を期待すべきではない。例えば、SQL Server でも 2008 までのバージョンは AWE を大きな空間に頼ることができた。2012年のバージョンから「AWEのサポートは廃止」とアナウンスされている。

ほか、Windows Vista 以降を対象に MEM_LARGE_PAGES オプションなどが用意されている。
「Windows Vista ,7 以降は VirtualAlloc を呼び出す際に、MEM_LARGE_PAGESやMEM_4MB_PAGES オプションを追加する」ことで大きい規模の領域を効率的に確保できるとのこと・・・

if ((lpTemp[0] = ::VirtualAlloc( ~~ ) ) == NULL){
} else {
}

の部分が読み辛いでしょうか。ここは、

lpTemp[0] = ::VirtualAlloc( ~~ );
if (lpTemp[0] == NULL){
} else {
}

と書いても意味は同じ。

if (○○== NULLではなくif (!○○と書く方がスマートなのでは!?」などの声が届きそう。
if 文が「単純な比較」の意味なら if (!○○ と書く方がスマート。ここでのif 文は「代入してから評価する」の意味。

VirtualAlloc で失敗した場合、NULL が返答される。もし、VirtualAlloc で確保できない場合は、最初の{} 内側のブロック、確保できた場合 は else {}の内側のブロックへと進む。
「メモリ確保のたびに確認が必要なの???」
との声も聞こえてきそう。
たしかに、教材やサンプルコードでは条件判断、「成功もしくは失敗」の確認を省いている例もたくさん見かけます。
規模の小さなモノやテスト的なコードの段階では、失敗せずに進んだのでしょう。それを前提に確認を省いているのか、あるいは、サンプルを載せたヒトが無頓着なのかもしれません。
今回のようなサイズのメモリ空間を一度に確保することは「お行儀の悪い」と揶揄されるものです。が、画像や音声を扱うアプリを組むとして、高速さを求めるならば「巨大なメモリ空間を扱いたい」と考えるのも当然。
巨大なメモリ空間を確保する際、成功を期待しないのが賢明。時には確保に失敗します。失敗したにも関わらず ( 強引に ) 次に進むことは、実生活の場ではお風呂の空焚きするようなトラブルのもと。
今回の例では、lRet = ERROR_OUTOFMEMORY;が失敗した時の対処。エラーコードを代入して次の作業へ到達しないようになっている。
トラブルを避けるために、ただ命令を羅列して闇雲に進むのではなく、「もし△△が成功したら次へ」のように安全確認しながら進むようにしたいものだ・・・
※ メモリ確保、解放を繰り返すごとにメモリの断片化が起こり、やがて確保に失敗するとの見解が多い。

VirtualAlloc 関数で確保した空間は VirtualFree 関数で解放する。
「ポインタが NULL でないことを確認してからメモリ解放関数を呼ぶ」
を守っていればトラブルを減らせる。VirtualFree 関数を呼び出す前に if (ポインタ変数) もしくは if (ポインタ変数 != NULL) の条件判断を行うのが良い。
今回は確保直後に NULL で無いことを確認してある。失敗を意味する NULL ならば解放する関数の部分に到達しない。よって省略可能。上記の例ではif 文による条件判断をコメントアウトしてあります。

参考までに HeapAlloc を用いた例。

    HANDLE hHeap;
    LPVOID lpTemp[2] = { NULL , NULL};

    if ((hHeap = HeapCreate( 0 , 0 , 0) ) != NULL){

        lRet = ERROR_INVALID_HANDLE;

    } else {

        if ((lpTemp[0] = (LPVOID) HeapAlloc(hHeap , HEAP_ZERO_MEMORY , __SIZE_DUPLICATE) ) != NULL){

            lRet = ERROR_OUTOFMEMORY;

        } else {

            if ((lpTemp[1] = (LPVOID) HeapAlloc(hHeap , HEAP_ZERO_MEMORY , __SIZE_DUPLICATE)) != NULL){

                lRet = ERROR_OUTOFMEMORY;

            } else {
                // ここに(3-3) 以降のコードを追加する

                lRet = S_OK;
            }

        }

        if (hHeap){
            ::HeapDestroy( hHeap );
        }

    }


※ VirtualAlloc の例と同様、HeapLock を抜いたコードとなっている。

HeapAlloc でメモリ確保する前、hHeap = HeapCreate( ~~ の部分でヒープを作成。
あまり大きな空間でないならば、GetProcessHeap 関数を用いて HeapCreate を省くことも可能。今回確保したいメモリのサイズはそれよりもはるかに大きいため、新しくヒープを作成する。

ほかにも、鋭いヒトから、
「HeapFree 関数が抜けているよ~!」などの声が届きそう。
「借りたモノは確実に返す」に従い malloc 関数で確保したメモリ空間は、free 関数 で解放する。先ほどの VirtualAlloc 関数で確保したメモリ空間は VirtualFree で解放。それと同様に HeapAlloc 関数 に対しても HeapFree 関数 を使ったほうが丁寧。
筆者の手元にある MSDN ライブラリによれば、
「Processes can call HeapDestroy without first calling the HeapFree function to free memory allocated from the heap.」
日本語版では
「ヒープから割り当て済みのメモリを解放する場合、最初に HeapFree 関数を呼び出すことなく、直接 HeapDestroy 関数を呼び出すこともできます。」
と記載されている。これが、HeapFree 関数 を省いても通用しそうな根拠。言い換えれば、
「ヒープを作成し確保したメモリ空間であれば、HeapDestroy 関数でヒープを破棄する際に全て解放される」
よって、解放系の関数をあれこれ設置せず、HeapDestroy 関数 に任せてしまうのが楽。

HeapDestroy 関数を用いる際も、「ヒープハンドルが NULL でないことを確認」すべき。アプリが異常終了する事態を追跡すると、HeapDestroy で 二重解放、多重解放が原因となるケースも多い・・・

(3-3) 開始と終了の時刻を取得

DWORD dwTm[2];

Sleep( 5000 );

timeBeginPeriod(1);
dwTm[0] = timeGetTime();

// ここに(3-5) 以降のコードを追加する

dwTm[1] = timeGetTime();
timeEndPeriod(1);

// ここに(3-4) 以降のコードを追加する

Sleep 関数は、アプリが安定状態になるまで待つ。単位はミリ秒、1/1000 秒。例えば1秒待たせたいならば 1000 を渡す。
昨今のIntel 製 CPU ではターボ状態が30秒ほど続くことがある。ターボ状態の最中に実行すると結果がバラつく。バラつき避けたいならば、Sleep 関数へ渡す値を 40000 程度、40 秒程度に設定すると良いのかも・・・

Windows 上で時刻を得る関数として挙げるとすれば、GetTickCount、timeGetTime、QueryPerformanceCounter や QueryPerformanceFrequency などなど。
ほかにも、タイムスタンプカウンタ を読み出す方法がある。タイムスタンプカウンタ を読み出したい場合、アセンブリ言語 の RDTSC 命令を使うのだが、少々古めの Visual Studio ではインラインアセンブラを使うことになる。Visual Studio 2005 より新しいバージョンならば __rdtsc() という関数が用意されており、アセンブリ言語に頼らなくとも可。
今回のようなテスト用途であればインラインアセンブラも差し支えないだろうが、ひとつのアプリを作るとなると、全般的な最適化が妨げられることがある。アセンブリ言語 、インラインアセンブラ を含んだコードは 32ビット向けアプリとしてならばビルド可能であるが、64ビット向けのアプリとしてビルドに失敗する。
タイムスタンプカウンタ を読み出す方法 は CPU の周波数が一定で動作していた時代には有効であった。昨今の省電力機能が搭載された CPU では、緩急に応じて周波数が変動し、得られる値にバラつきが生じる。よって却下。
GetTickCount や QueryPerformance もクセが強い。などなどの理由から今回はtimeGetTime 関数を選択。

測りたい処理の開始前、処理終了の2箇所の timeGetTime 関数を呼ぶ。走る競技のスタートとゴールの瞬間ストップウォッチのボタンを押すようなイメージだ。終了時の値から開始時の値を引けば経過時間となる。

timeGetTime 関数の性質上、開始時の値が終了時の値を上回ることもある。当然、結果が異常な数値となる。そのような場合は計測結果を標本から外せは済むこと・・・

timeGetTime 関数 を覆うように timeBeginPeriod や timeEndPeriod 関数を追加。
これらの関数は最小タイマ分解能を高める役割がある。反面、timeBeginPeriod は副作用があるのも有名。ここでの副作用というのは、キビキビ動くのと引き換えに省電力機能が効き難くなる。
メモリの alloc と free が対になるように、timeBeginPeriod を用いたならば timeEndPeriod を忘れずに・・・

筆者が過去に遭遇したケースの中に、「複数ソケットのマシンにおいて、timeGetTime で得られる値が、実際の経過時間の 2倍や 4倍 になってしまう」という現象があった。複数ソケットのマシン複数コアのマシンを誤解するヒトもいる。


※ これは2012年7月25日に掲載した記事の画像です

SMP (Symmetric Multiprocessing) 、「対称型マルチプロセッサ」や 「対称型マルチプロセシング」と訳される。過去記事の画像にあるように複数のCPU が 搭載されている状態。
現在主流のCore i7 , i5 , i3 や Core 2 Duo ,Quad などの CPU はひとつのソケットで複数のコアを持っている。当時はひとつのソケットでひとつのプロセッサ、ひとつのプロセッサでひとつのコアというのが主流であった。

通常は 1秒イコール1000ms。2つの timeGetTime の差を得る場合、例えば60秒ならば 60000 となることを期待する。ところが、挿しているプロセッサの数を掛けた数値が返ってきた。
「搭載されている CPU コア数を取得して割り算すれば良いのでは!?」
などの意見も出そうだ。
そのマシンで10000 を指定して Sleep 関数を呼び出したところ、きっちり10秒間休憩することが判った。ということで、Sleep 関数を timeGetTime で挟み、Sleep した時間と timeGetTime で得られる値との比率で割る策を処した・・・

(3-4) 経過時間を確認

いまだ、C 言語の教本では printf や puts でコンソール画面に出力する等が載っているのではないだろうか。Windows 用アプリならば、wsprintf で文字列を書式化し、MessagoBox を表示するのが単純。
※ wsprintf と 似た wprintf も存在するのでお間違えなく。

wsprintf((LPTSTR) lpTemp[0] , _TEXT("time = %d \0") , (dwTm[1] - dwTm[0]) );
MessageBox(hOwner , (LPCTSTR) lpTemp[0] , _TEXT("job done.\0") , MB_OK);

文字列を書式化したいなら、古くからの sprintf 関数 などがある。ただし、Visual Studio でこれらの古い関数を用いるとたくさんの警告が出る。すでに、車輪の再発明 (3)で触れたとおり、旧来からの関数を「_s」付きの別の関数、sprintf 関数 ならば sprintf_s 関数に置き換えれることで警告を減らせます。

MessagoBox 関数でダイアログボックスを表示する。
PC を長く使っているヒトに何か相談された際、「ダイアログボックス」という単語が通じない場面に遭遇する。そもそもダイアログボックスが何を指すか判っていないヒトもいれば、ダイアログボックスとウィンドウ・フォームを混同しているヒトもいる。



おおざっぱに言えば、意思確認のために表示されるモノを指す。
例えば、メモ帳で何か入力したとしよう。メモ帳を終了しようとすれば、保存するか否かの確認される。

MessagoBox 関数 の戻り値を通して、ユーザがクリックしたボタンの値を知ることができる。今回は [ OK ] ボタンのみなので、どのボタンをクリックしたかの確認は不要。

「ダイアログボックスを表示させるのは面倒」というのなら、MessagoBox( ~ の部分を
::SetWindowText(hOwner , (LPCTSTR) lpTemp[0]);
に置き換えることで、フォームのタイトルバー上に結果が表示されるハズ。

ところで、MessagoBox や wsprintf の引数に lpTemp を指定している部分が気になるヒトもいることだろう。何を意図したかといえば、すでに動的に確保したメモリを使いまわしているに過ぎない。
たしかに、出回っているサンプルコードでは
TCHAR szMsg[256];
等と文字列変数を定義し、文字列を作成する作業域に指定する例も多い。
重複するが、AMD や Intel 製 CPU 、Windows 上で動かすアプリを想定しているなら構い。スタック、つまり、自由に使える作業域が 1MB なので通用する。
昨今はそれ以外のプロセッサでも Windows が搭載されるようになってきた。AMD や Intel 製 CPU 以外のプロセッサではスタックが小さい場合があり、エラーとなるかもしれない・・・

(3-5) 測りたい部分
2箇所の timeGetTime を置き、その間に速度を測りたい部分を設ける。

ここで処理したいのは メモリ - メモリ間のデータ転送。ひとまず、古くからのコードを設けておく。

CopyMemory(lpTemp[1] , lpTemp[0] , __SIZE_DUPLICATE);

データ転送を行う関数はいくつかある。C/C++ 言語の教本に登場する memcpy 関数、Windows 上のアプリと限定すれば CopyMemory 関数。今回は CopyMemory を選択。memcpy と CopyMemory 関数との違いは戻り値が無い点。

PC は時代とともに高速化されてきた。CopyMemory 関数を一回実行するだけでは、一瞬で完了し、結果を把握し辛いかも。1回実行するだけでなく、10回なり、100回なり繰り返して結果を見るのがよいだろう。例えば 10回くり返したいなら
SIZE_T i;
for(i = 10 ; i ; --i){

    CopyMemory( ~ );
}
のような感じ。for 文 や while 文で繰り返す。カウンタをマイナスする意は別の機会に触れたい。
くれぐれも、繰り返し回数を増やしすぎないように。Windows 7 やそれ以降のOSでは一定の時間アプリが反応しないと異常と判定されることがある。

ちなみに、筆者がアプリを組む際、( 反応無しになるのを防ぐことを目的として ) 長時間要すると判っている特定の箇所にメッセージをポンプする機能を埋め込むことがある。それはVisual Basic や C# の DoEventsメソッドのようなモノで、PeekMessage と DispatchMessage 関数の使い方が判ればそれほど難しくない。
今回の例で言えば、メッセージをポンプする機能は _tWinMain( 関数内の後半に含まれているメッセージループのコードがそれに近い。メッセージループとは、while( 文で始まり、GetMessage( ~~ ) とその内側に TranslateMessage や DispatchMessage が含まれている部分。GetMessage 関数は何かイベントが発生するまで待ち続け、while 文でイベント待ちやディスパッチを繰り返す。
DoEvents 相当の関数を考えるならば、if (PeekMessage( ~~ ) ){}の形に変え、{}の内側は メッセージループと同じに構えればOK。メッセージループと異なるのは、2番目の引数として指定するウィンドウのハンドル値で、これが NULL と変わる点。ハンドル値を特定せずに NULL としておくことで、ほかのアプリやプロセスへメッセージが届くようになる。

この辺でビルドして実行してみよう。アプリのフォーム上にある、[ 開始 ]ボタンをクリックして計測開始。



画像は 141msとなっている。ビルド直後に実行すると若干遅れが生じる。何度か繰り返すと値が安定する。
Windows に用意されている CopyMemory 関数は 124ms。旧来からの memcpy 関数は 127 ms。ほぼ誤差の範囲。

ここまでで、 段階(3) の古典的な方法とその速度を測る部分が出来た。次のステップは、速度向上を目指し、データ転送部分を加工してゆく・・・

ところで、速度を調べる前に確認しておくべきことがある。ベンチマーク系のアプリを使うヒトにとっては常識的なコトであるが、一応記す。
作業を全力で実行できる状況で測るようにしたい。その場合、余計な割り込みや負荷を減らすことが重要。
例えば、タスクマネージャを見て不要と思われるプロセスを停止する。特にネットへのアクセスは割り込みが増える。LANケーブルを抜くだけではなく、デバイスマネージャからネットワークアダプタを停止すると良い。
さらに、省電力機能をoff にする。そうしないと、全力で実行されない可能性があり、得られる結果にバラつきが生じ易い。
コントロールパネルから、省電力設定を解除しただけでは省電力の仕組みがoffにならないこともある。
Intel 製 CPU を搭載したPC ならば、UEFI や BIOS の設定画面 でEIST や C3/C6ステートを無効、AMD 製 CPU を搭載したPC ならば Cool'n'Quiet が無効になっているか確認。

長くなりましたので続きはまた後日・・・

本日も最後までご覧いただきありがとうございます。

「つまらなかった」「判り辛った」という方もご遠慮なくコメント欄へどうぞ

テーマ : プログラミング
ジャンル : コンピュータ

検索サイトからお越しの方へ
検索サイトからお越しの方は、ブラウザのアドレス欄vitalaboloveおよび、fc2.comが含まれているかご確認ください。
含まれていない場合、偽サイトを閲覧なされている可能性があります。

偽サイトは、当ブログの文字部分や画像部分が有害サイトへのバナーと置き換わっているようです。
プロフィール

Author:Vitalabolove
ご訪問ありがとうございます。
店長を任されておりますVitalaboloveです。

コメントはお気軽に。
今のところリンクフリーですが、あと数日でとりやめます。

画像データ、文言の引用は事前連絡くださるようお願い申し上げます。事前連絡の際は、左下、メールフォームを経由をご利用ください。

最新記事
カレンダー
05 | 2014/06 | 07
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 - - - - -
カテゴリ
ランキング
いつも応援いただきありがとうございました。ただいま休養中につきランキングへ参加していません・・・

フリーエリア
内緒話などはおきてがみをご利用ください。
月別アーカイブ
メールフォーム
掲載された記事について、ご不明な点はここからお問い合わせください

名前:
メール:
件名:
本文:

最新コメント
最新トラックバック
スパムと思われるトラックバックは削除しました
QRコード
QR
上記広告は1ヶ月以上更新のないブログに表示されています。新しい記事を書くことで広告を消せます。