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

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


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



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

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

スポンサーサイト

上記の広告は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 関数 を書き換えてみる・・・と続けたいところですが・・・

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

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

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

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

コメントの投稿

非公開コメント

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

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

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

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

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

最新記事
カレンダー
05 | 2017/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ヶ月以上更新のないブログに表示されています。新しい記事を書くことで広告を消せます。