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

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


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



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

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

スポンサーサイト

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

[未掲載分] 「除算が遅い」の補足 (9)

「除算が遅い」の補足 (8)の続き。多くのヒトにとって除算が速い遅いといった話題は興味が沸かないことだろう。



例えば、音遊びに使うアプリを作るとしよう。これは、音とび ( クリッピングが生じた音声データ ) を修正するために組んだアプリである。
もとの音データは整数値で収納されている。アプリはそれを読み出し、乗算・除算・三角関数などを通して処理して保存する。



アプリを使う側の視点で考えてみよう。演算が遅いということは待ち時間が増える。画像や音を編集するアプリを利用する際、多かれ少なかれ待ち時間が発生する。多く待たされることはストレスにつながる。数十秒で終わるのか、はたまた、一時間近く待たされるのか・・・


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

※ これを下書きしたのは2012年夏の終わり頃です。
ご覧いただいている時期によっては状況が異なっている可能性があります。
下書きした頃の環境は、OS - Microsoft Windows 7、開発環境 - Visual Studio 2008 , C/C++ 言語。
Intel 80386 ( i386 ) の流れを汲む Core i7 や Pentium や Atom 等 の CPU もしくは互換品を前提としています。


予め述べておくが、
C/C++ 言語でいうところの変数の型や型変換など、基礎的な事柄の理解が曖昧なままここに辿りついたヒトもいることだろう。この先の内容を飲み込むのは辛いと思われる。ほかのプログラミング言語を用いるにしろ、そのようなヒトは除算の高速化うんぬんの前に基礎的な事柄を習得した後、再度ご覧頂ければ幸いである・・・

少しおさらい。
「除算が遅い」の補足 (6)2組分の除算を行える~~~待ち時間はほぼ一緒。と述べた。
並列に実行できる、できないに関して手順を練り直すことで、さらなる高速化を目指せる。

なぜ XMM レジスタの明示的な利用するのかについては、「除算が遅い」の補足 (3)「除算が遅い」の補足 (4)で綴ったように、
レジスタ数に余裕があるか否かで実行速度が左右されるからである。

※ 汎用レジスタは標準装備 ( もともと備わっていた )、XMM レジスタは後から追加された。

32ビット版の OS で実行するアプリを作成した際、空きレジスタの不足を補うようなコードが生成されることがあり、速度低下の要因になる。64ビット版の OS 上で32ビット版 のアプリを実行することは可能であるが、これは解消されない。
つぎに、64ビット版の OS 向けにアプリを作成した場合、32ビット版に比べ汎用レジスタ数に余裕がある。汎用レジスタで64ビット数値を直接扱える。したがって、XMM レジスタを積極的にメリットが薄いように感じてしまう。
64ビット版のアプリでは FPU を介した演算がサポートされない。西暦1993年頃に登場した Pentium から FPU ( 浮動小数点演算処理装置 ) が標準搭載されるようになった。そのほか、XMM レジスタでの演算は、FPU を介するのに比べ、同時に2組分の演算をこなせるので待ち時間を減らすことに繋がる。FPU の内部での演算精度が 80ビットである点は魅力的ではあるが・・・

しかし、前回綴ったように、SSE2 命令体系の中に整数の除算が含まれていない。
誤解が生じるといけないので、「命令が備わっていない。」と表現した方が適切だろうか。ハードウェア内部には除算を行う回路が備わっていても、それを呼び出す命令が存在しない。
もちろん、ソフトウェアレベルでの代替は可能である。しかし、「除算が遅い」の補足 (1)「除算が遅い」の補足 (3)で触れたように、ソフトウェアレベルでの代替処理に速さ求めても仕方がない。
その後、SSE4.2 に至るまで整数の除算命令は追加されなかった。では、整数の除算に関して諦めるしかないのだろうか?

前回XMM レジスタで倍精度浮動小数点値と 32 ビット整数値、もしくはその逆の変換が可能とヒントを載せた。
C/C++ 言語でいうところの型変換 ( キャスト) が判っていれば理解し易いハズ。
いま、どの型で演算しようとしているのか???等の基礎が曖昧なままアプリ作成を続けているヒトもいることだろう。昨今はプログラミングに親しみやすいようにとの計らいで、その辺を深入りせずともアプリを組めるようになってきた。とはいえ、上の段階に歩を進めようとした際に戸惑うことがないよう、基礎固めは重要・・・

この記事は Core i7 や Pentium や Atom 等 の Intel 80386 ( i386 ) の流れを汲む CPU を前提にしている。
一般的な PC の CPU は自動車でいうところのエンジンに該当する。西暦1993年頃に登場した Pentium から FPU が標準搭載され浮動小数点数 ( 実数 )を直接扱えるようになった。それまでは 別売りの FPU を追加するか、FPU を介しないソフトウェアによる代替で処理するようなアプリを組む必要があった。
もっと遡れば、CPU が直接扱えるのは 整数に限られていた時期もある。諸説あるが、電子計算が行われるようになったのは西暦1946年頃と言われている。現在用いられている単精度や倍精度浮動小数点数演算の標準化が行われたのは西暦1985年ごろ・・・

整数しか扱えない不便さは想像し難いだろうか。
例えば、本体価格10円、そこに5%の税金が乗るとしよう。人間の頭では一個10.5円と考えることができる。
いちばん小さい通貨の単位が1円であるならば、切り捨てて10円、切り上げて11円。前者では売る側が0.5円を負担、後者なら買う側が多く支払うことになる。どこか不公平。
もちろん、毎回2個一組での売買に定め、値段は21円に設定するなどの策も考えられるのだが・・・



XMM レジスタを用いて整数から倍精度浮動小数点値に変換する、または、その逆の命令 (倍精度浮動小数点値から整数に変換する ) を知りたい場合、
・IA-32 インテル アーキテクチャ ソフトウェア・デベロッパーズ・マニュアル
・インテル エクステンデッド・メモリ64 テクノロジ・ソフトウェア・デベロッパーズ・ガイド等の書物が役立つ。もちろん無料。それら資料の入手に関しては過去記事車輪の再発明 (8)をご覧あれ。

「変換」は「convert」「conversion」「transformation」。IA-32 インテル アーキテクチャ ソフトウェア・デベロッパーズ・マニュアルは4巻。そのうち、中巻A:命令セット・リファレンスA-M。おそらく、「conv~~」「cvt~~」の項を眺めると何か見つかる。



CPU が直接解釈できるような機械語コードやCVT○○○といった擬似コードを覚えるのは苦労する。
C/C++ 言語でアプリを作成するなら組み込み関数を利用する。開発環境が Visual Studio だとすれば、「_mm_cvtepi32_pd」 と 「_mm_cvtpd_epi32」 の組み込み関数を用いることが可能。前者が整数から倍精度浮動小数への変換、後者はその逆。

「_mm_ ○△□」といった関数名を覚えるのが面倒!?!?!?
それに関しては「除算が遅い」の補足 (6)で触れたように「_mm_ ○△□ _pd」といった組み込み関数は「○△□」の部分が掛け算なら 「MUL」 、 割り算なら「DIV」と理解できる。同様に、数値を変換したい際に用いる組み込み関数の名は
「_mm_cvt」、「変換もと変数の型」、アンダースコア ( 下線 )、「変換される変数の型」の順に並んでいると理解できるハズ。

例えば、「_mm_cvtsd_si32」という組み込み関数であれば、倍精度浮動小数点値を符号付き 32 ビット整数値に変換するのに用いる。ほかにも、よく見ると



「CVTPD2DQ」のほかに「CVTTPD2DQ」という命令が載っている。違いは「CVT」と「CVTT」。
組み込み関数の方も「_mm_cvtpd_」「_mm_cvttpd_」といった具合でなんだか紛らわしい。見た目の違いは「cvt」「cvtt」。
「t」が付く付かないで変換される値が変わってしまうので注意が必要。
「_mm_cvt」の方は現在設定されている丸めモードに従って変換、「_mm_cvtt」のように「t」が2つ付く方は切り捨てて変換となる。

丸めモードは「直近値への丸め」、「切り上げ」、「切り捨て」、「ゼロ方向への丸め」があり、プログラマーが任意に指定できる。既定の丸めモード、つまり何も指定しなかった場合は、一番近い値に丸められる。
アプリを作って実行した際、演算結果が期待と大きく違ってしまうヒトはこの辺の事柄が飲み込めていないのかも。

丸めモードを指定するのはそれほど難しくない。Windows 用のアプリをC/C++ 言語 で作成するとして、FPU を介した演算ならば、_control87、_controlfp などのランタイム関数を呼び出すことで丸め動作を変更できる。

XMM レジスタを介した演算では _mm_setcsr 関数で同様の指定が可能である。
念のため書いておくが、_mm_setcsr 関数を用いて丸めモードを自由に変更できるとはいえ、演算ループが終了したら変更前に戻すのが無難である。



_mm_getcsr 関数を用いることで制御レジスタの内容を読み出せる。読み出した値をどこかに保持しておき、演算の繰り返しが終わり次第、制御レジスタの内容を元の値に戻すのが良いだろう・・・



実数で演算し結果を整数に変換する場合、実行速度を左右する項目があります。・・・ちなみに、この話の旨は XMM レジスタの活用であって、FPU の活用ではありません・・・
開発環境が Visual Studio 、32ビット版のアプリを作成する、演算は FPU を介するとしよう。整数に変換する際、何も指定しないと _ftol や _ftol2 といったヘルパー関数を呼び出して処理する。先に述べた通り、ソフトウェアによる代替処理は速くない。
もちろん、FPU が直接変換して値を返すことも可能である。重複するが、FPU が標準搭載されるようになったのは Pentium の登場からである。初期の Pentium において演算回路に欠陥を抱えたモノが出回った。ハードウェアレベルでの間違いを正すためにヘルパー関数を呼び出す。

逆にいえば、FPU による 直接変換ならば速度低下を避けられそうだ。標準仕様による _ftol や _ftol2 といったヘルパー関数を呼び出すのが遅い原因だとすれば、呼び出さないように指定すれば改善されるかも。
Visual Studio で/QIfistオプションを指定することで浮動小数点型から整数型への変換が必要なときのヘルパー関数 _ftol を呼び出さなくなる。つまり、FPU による 直接変換を行うようなアプリが生成される。たいていの場合、遅い部分が改善される。

Visual Studio で何らかのオプションを指定したい場合、プロジェクトの「プロパティページ」 ダイアログ ボックスを開く。ところが、/QIfistオプションを指定する項目は見つからない。



プロジェクトの [プロパティ ページ] ダイアログ ボックスを開き、左ペインの[C/C++] をクリック。続いて、[コマンド ライン] プロパティ ページをクリック。



[追加のオプション] の入力欄に「/QIfist」と入力。

※ 初期の Pentium プロセッサーでなくとも演算結果が期待通りにならないリスクもあります。よって、このオプションを使うかどうかは慎重に・・・

あ、忘れるところでした。_ftol や _ftol2 でエラーになる件。
コンパイラとリンカーの相性が悪いとアプリ作成時にエラー。原因はコンパイラとリンカーのバージョンが合っていないこと。浮動小数点型から整数型への変換で _ftol2 関数 が使われるようになったのは西暦 2003 年頃リリースされた Visual Studio .net や同 2003 から。それ以前の Visual Studio 6.0 等では _ftol 関数 を用いたアプリとなる。
大雑把に言えば、Windows XP を念頭に置いた開発環境とそれ以前の Windows 98 や NT 4.0 を対象とした開発環境といったところ。実は、Microsoft Visual Studio 6.0 と Visual C++ Toolkit 2003 を組み合わせることが可能であった。この組み合わせにおいて、コンパイラが _ftol2 を指示してしまうが、リンカは古い方を探しに行ってしまい、結局エラーとなる。

ソースファイルの先頭の方に
extern "C" long _ftol2( double dblSource ) {return _ftol( dblSource );}
・・・といったオマジナイを加えることで解消できたような・・・

開発環境を整えておくことでこのようなトラブルは防げる。Visual Studio の新バージョンが登場すると導入したくなる。新バージョンを導入する際、ついつい古いバージョンも残しておきたくなる。新旧共存させたい気持ちもわからなくはない。新しいモノに不具合はツキモノ。とはいえ、古いモノでは機能が足りない感が否めない。さらに、複数の PC に分けるのも手間がかかる。
一台の PC で済ませたいならば、VMWare 等の仮想環境を検討すると良い・・・



さてさて、XMM レジスタを活かした除算の高速化に話を戻しましょう。
冒頭に載せたようなアプリを組みたいとします。想いつく流れをおおまかに示すと
・整数値のデータを読み出す
・整数から倍精度浮動小数点値へ変換
・浮動小数で演算
・演算結果を整数に変換
・結果をメモリにストア ( 格納 )する・・・

・・・といった具合でしょうか。

長くなりましたので今回はこの辺で・・・
スポンサーサイト

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

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

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

[未掲載分] 「除算が遅い」の補足 (8)

「除算が遅い」の補足 (6)「除算が遅い」の補足 (7)で、実数 ( 浮動小数点数 ) の除算でさらなる速度向上を目指すについて触れた。



今回は、整数の除算について・・・


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




A = B ÷ C という式において、C ( 除数、割るほうの数 ) が定まっている場合、「除算が遅い」の補足 (2)で、述べたように
・整数の除算ならば、「乗算とシフト」に置き換えると良い。

除数 が定まっていないとして話を進めよう。「除算が遅い」の補足 (6)で、浮動小数点数の演算を多数繰り返す場合、
・追加された XMM レジスタを活かし同時に複数の演算を行うことや、
・並列に実行できるように練り直すことで、さらなる速度向上を期待できると述べた。昨今のCPUであれば、依存関係が無い命令が続く場合、同時に実行できる。

ということは、整数の除算についても、それと同様の手法が使える???

SSE2 命令に対応した CPU であれば、XMM レジスタで整数の演算も可能である。
複数の倍精度浮動小数点数を扱うには、__m128dといった変数の型を用いた。複数の整数を扱う際に用いる変数の型は__m128i



128ビットの XMMレジスタで32ビット幅の整数値を4組、



もしくは、64ビット幅の整数値を2組同時に扱える。ほかにも、16組の8ビット整数や8つの16ビット整数といった組み合わせも可能である。

実は、加算、減算、乗算の命令は用意されている。しかし、XMM レジスタで整数の除算を行う命令が無い。

C/C++ 言語で XMM レジスタを明示的に使う場合、浮動小数点数の演算では_mm_ ○△□ _pdなどの組み込み関数を用いる。乗算や除算は_mm_mul_pd_mm_div_pd
整数の加算、減算、乗算はそれぞれ、_mm_add_epi32 や _mm_add_epi64、_mm_sub_epi32 や _mm_sub_epi64 、_mm_mul_epu32 の組み込み関数が用意されている。しかし、除算に直結するような _mm_div_epu32 や _mm_div_epu64 が用意されていない・・・
たしか、ここはリザーブ扱いになっていたハズなので SSE2 が登場したての頃将来的に実装する予定だったのかもしれません。その後、SSE3 から SSE4.2に至るまで整数の除算命令は追加されませんでした。XMM レジスタを用いた整数の除算命令が備わることを望む声も少なくなかったハズ・・・

・・・ということで、整数の除算部分を自前で組む???

昨今流通している CPU では当然のようにハードウェアレベルで除算が可能。汎用レジスタで除算する仕組みが備わっている。
これを書いている時点でPC 用として普及している CPU は Core i7 シリーズ。Core i7 は 1980年代後半に普及した Intel 80386 ( i386 ) の流れを汲んでいる。i386 は 32ビット版 の CPU として登場した。遡ると、8086 や 80286 などの16ビット版のプロセッサをもとに32ビット版への拡張が施された。



8086 には登場時から除算機能が備わっている。つまり、汎用レジスタによる整数の除算が可能である。8086 の説明で「~~~ 8080のアーキテクチャを16ビットに拡張し、乗除算などの命令を強化したCPU ~~~」などの記載を見かける。
「アーキテクチャうんぬん」との表記に戸惑うヒトがいるかもしれない。要はIntel 8080 という 8ビット版 CPU の「基本設計概念」を受け継ぎ 16ビット版 CPU として登場したといったところ。
細かく言うと、Intel 8080 の後継、拡張版としては Intel 8085 や Z80 などの 8ビット版 CPU が存在した。Z80 は Intel 8080 の開発に携わった者たちが独立起業して開発されたモノだが、当時は 8080よりもZ80を搭載した PC も多く流通していた・・・

ここで肝心なのは CPU の進化うんぬんよりも、「乗除算などの命令を強化した」の部分。かつては除算命令が備わっていなかったことを指している。その頃の機器は、データの読み書き、加算、減算とシフトが出来る程度だった。誤解が無いように加えておけば「除算命令が備わっていない」「除算できない」ではない。
自前で組むか、既存のライブラリに頼るかプログラマの力量次第だが、除算命令が備わっていなくてもソフトウェアレベルで何とかすることができた。

そもそも、昨今の CPU で除算命令をどのように処理しているのだろうか。
大雑把に言えば、ひたすら引き算を繰り返す。乗算 ( 掛け算 ) はたくさんの足し算に置き換えられることが判ればこの辺は易く理解できるハズ。

古くからの手法として、引き放し法や引き戻し法が有名である。また、0 と 1 で表される2進数の除算に最適なFast recursive divisionが発表されている。この元を書いたのは西暦2012年。Fast recursive division が発表されたのは西暦1998年頃であり、それから10年以上経つが、ハードウェアレベルで備るにはまだ時間が掛かるかも。

ひとことに引き算を繰り返すと書いただけでは想像しずらいかもしれません。
例えば1234÷5を計算するとしよう。予め答えを書いておくと246。
ひたすら引き算を繰り返すといっても、1234から5を引いて1229、まだ5より大きいから5を引いて1224~~~といった手順は非効率。
私たちが子供の頃習うのは、最上位の1は5より小さいので引けない。上から2桁の12から5を2回引いて、余りが234。次に2から5は引けないので23から・・・といった具合である。
被除数 ( 割られるほうの数 ) に対して除数 ( 割るほうの数 ) をそれぞれの位に合わせながら解いてゆくハズ。

2進数を理解すればシフト、減算、比較命令で除算処理を実現できそう。その前に、2進数とは何ぞやという状態でここを訪れたヒトは遅くない除算をご参照あれ・・・
1234を 2進数 ( 0 と 1 の組み合わせ ) で表すと0100 1101 0010 。同様に5を2進数で表すと 0101。

最初に被除数と除数を比べます。小さければ 0、等しいなら1の答えをもって終了。そうでない場合は位をそろえながら引き算を繰り返します。

まず、位を合わせます。


最上位の何ビット目が1であるかを探し、除数をシフトして位を合わせます。使うのはビットスキャンとシフト命令。



引けるか否かを比べ、可能なら減算。ここでは 0100 から 0101 を引けないので位を下げるため1ビット右シフト。使うのは比較と分岐、シフト命令。



さてさて、今度は引けそうです。減算命令の出番。引いた結果が大きければ、さらに1ビット右シフトして~~~といった感じで引けなくなるまで繰り返します。

答えは2進数で 1111 0110。 不慣れなヒトにとって 0 と1 の組み合わせで0110 等の表記は見辛いことでしょう。理解するには、一番右を1として、2番目のビットが1なら2,3番目のビットが1なら4,4番目のビットが1なら8 を足してゆくことで変換し易くなります。例えば、2進数の 0110を10進数に変換する場合 4+2。
一度4ビット分ずつの16進数に直すと判り易いかもしれません。1111 は 8*1 + 4*1 + 2*1 + 1*1 となり 16進数で F , 10進数で15、同様に 0110 は 8*0 + 4*1 + 2*1 + 1*0 となり
16進数で 6、つまり 2進数で1111 0110 は16進数で F6となります。これは 15*16 + 6 となり、答えは10進数で246。

この辺の仕組みが判ってくれば、256ビット幅や512ビット幅の数値といった汎用レジスタのビット幅を超えるような数値の演算コードが書けるようになるかもしれません。
とはいえ、ソフトウェアレベルでの代替処理はハードウェアレベルで備わっているのに比べ高速さを求めるような場面では不利。かつて、「除算が遅い」の補足 (1)でソフトウェアレベルでの除算を代行するので遅くなる件を取り上げました。
32ビット版の OS において、途中結果が汎用レジスタの幅に収まらない ( 64ビット幅の整数など ) 場合は、割り算命令に相当する関数を呼出す ( ソフトウェアレベルでの代替 ) ので遅くなる。
同じ計算式のまま64ビット版の OS 上で64ビット版向けアプリとしてビルドすれば、整数の除算部は汎用レジスタを用いた命令に翻訳される、つまり、ハードウェアレベルでの処理が選択される。実行速度を低下させる原因を取り除ける。
32ビット整数値はおおよそプラスマイナス21億、64ビット幅の整数はさらにその42億倍。実際のところ、一般的な PC で必要とされる整数の範囲として不足はない。それでも足りないような高度な演算が必要なのであれば、ソフトウェアレベルでの高速化についてあれこれ調べ巡るよりも、高度な演算に特化した機材を調達することを考えた方が良いだろう・・・

さてさて、SSE4.2に至るまで整数の除算命令が追加されなかった、というよりも見送られてしまった謎が解けそうだ。
Pentium 4 でも 西暦2004年頃リリースされた、通称 Prescott と呼ばれる第三世代より SSE3 の命令が実行可能になった。それと同時に、64ビット環境への対応が施され、つまり、64ビット版のOS やアプリを実行できる仕組みが追加された。
64ビット環境が普及すればハードウェアレベルでの演算に切り替わることにより遅い部分が解消できる。64ビット版で汎用レジスタで扱える。そのほかにもXMM レジスタで 整数の除算命令を行えるような改良・追加も可能だったと推測できる。しかし、その分回路は複雑になり、さらに多くの電力を消費するような構造になる。



当時、Pentium 4 シリーズで消費電力の高さ、言い換えれば、燃費の悪さが問題視されていた。発表されたモデルのうち燃費の悪さ等が重り、出荷されなかったモデルもある。この辺を振り返ってみれば、追加できそうな機能のうちいくつかが省かれてしまったのもいたしかたない。

ではでは、32ビット環境において整数の除算を高速化しようと考えないほうが良い!?!?!?
いえいえ。少し発想を転換すれば道が見つかるかも。ヒントを挙げるとすれば、XMM レジスタで倍精度度浮動小数点値と 32 ビット整数値、もしくはその逆の変換が可能な点・・・

長くなりましたので今回はこの辺で・・・

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

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

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

[未掲載分] 「除算が遅い」の補足 (7)

「除算が遅い」の補足 (4)「除算が遅い」の補足 (5)でお手軽に新しく追加された機能を活かすアプリを作る方法を述べた。全てお任せでは物足りないこともある。そこで、前話にて
高速化したい箇所のみ新機能を活かすようなソースコードに書き換えることにより、さらに高速化が狙えるのでは!?!?!?
を触れた。通常アプリを作成すると、新機能が備わっていない PC ( 1990年代中頃の PC ) でも 動く機械語コードが生成される。



生成されたのは fdiv など FPU ( 浮動小数点演算処理装置 ) を介して演算するようなコード。これを、明示的に 新機能を使うように書き換え、さらなる高速化を狙う。



divpd が2命令続いている。divpd 命令ひとつにつき2組の除算を同時に行うことを意味する。
並列、つまり、同時に複数の作業を進めるようなコードが生成される・・・


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


前話の続きとして、整数の除算の話に移りたいところであるが、後に回したい。

先に述べた、
並列実行されるようなコードが生成された・・・ゆえに、高速化されるハズ・・・と安堵できないケースもある。

実生活の場でも、命令を出す側、受ける側の想いが異なることは多々ある。受けた側がどのような手順で遂行するかを気にするよりも、オーダーされた通りの結果が出たかが大事。

前話で、
2組分の除算を行える~~~待ち時間はほぼ一緒。~~~※ 一部のCPUを除く。
という表現を用いた。「一部のCPU」と記しただけでは曖昧なので、実際を挙げます。
かつて、モバイル向けに開発された CPU の中に同時に2組分の処理を行わないモノがリリースされていました。
筆者はかつて、モバイル向け ( ノートPC 向け ) にリリースされていた Pentium M というCPU をデスクトップ PC として使っていました・・・



ひとくちにモバイル向け CPU と書くと紛らわしいので補足。
デスクトップ用の Pentium iii や Pentium 4 を基にノートパソコン向けにチューニングされた CPU もリリースされていました。それらは、Pentium III-M や Mobile Pentium 4 processor や Pentium 4-M と呼ばれていました。これら Pentium III-M や Pentium 4-M がデスクトップ用 CPU を基にノートパソコンで特化させたモノ。

画像で載せたのは、通称 Dothan と呼ばれる Pentium M。こちらはもともと、開発段階からモバイル用途を意識していた、というよりもモバイル専用としてリリースする予定だった。ところが、リリース後、高性能さが受け、やがてこれをデスクトップ用としても使えるように対応マザーボードが出荷されるに至った。
「なぜ、わざわざモバイル向けの CPU をデスクトップ用に???」と不思議に感じるヒトもいることだろう。理由としては、低発熱のモバイル向け CPU は静音化や小型化の面で有利だったから。
同世代の デスクトップ用として流通していた CPU は Pentium 4。



Pentium 4 は消費電力が高いとされ、ひと世代前の Pentium iii と比べ 2 ~ 3倍強。その分、発熱量も増大する。Pentium 4 で消費電力が増大してしまったというよりも、それと引き換えに高速な CPU を投入せざるをえなかった。Pentium 4 が登場した背景を考えると仕方のないことであるが、ひと世代前のPentium iii が全盛の頃、ライバル ( 互換品メーカー ) との間で高クロック競争となり、やがて追い抜かれてしまった。Pentium iii の動作クロックを向上させることも検討されたようだが、限界があり、ある程度の高さから動作不良を起こす。そこで、巻き返しを狙い、燃費が悪くてもライバル製品に対抗できる CPU をとして Pentium 4 がリリースされるに至った。
PC を 自作する者にとって、よりワット数が高い電源ユニットや、発熱対策としてより強力な冷却ファンを用意する必要があった。つまり、静音化や小型化とはかけ離れていった・・・

Pentium 4 が表舞台を賑わせている頃、その裏で Pentium M の開発が行われていました。ひと世代前の Pentium iii に磨きをかけ、Pentium 4 で見過ごされた消費電力を最小に抑えるための工夫も盛り込まれました。それはその後に登場する Core 2 Duo / Quad や Core i7 の礎となってゆきました・・・



Pentium M は Pentium 4 と比べ省電力であるほか、クロックあたりの性能が高く、同じ動作クロックであれば 1.5倍ほど高速といわれていました。しばしば、Pentium M の2GHz、Pentium 4 の2.8 GHz がほぼ互角との記載を見かけます。実際に両者を使っていた上で書くと、Pentium M を搭載したデスクトップ PC の方が静かで迅速と感じたものです・・・

そろそろ、除算のお話に移りましょう。
Pentium M は2組分の演算を同時に行えず、2回に分けて処理していたような感がありました。
「除算を2回行う」と「ひとつの命令で同時に2組の除算」の待ち時間がほぼいっしょ。
※ この記事をアップする際にあたり、手元に残っていた過去に計測した数値も確認しました。

それではかえって遅くなるのでは!?!?!?と誤解が生じるかもしれないので補足。
Pentium M は Pentium 4 と比べ除算の待ち時間が大幅に短縮されています。概ね半分か30%くらい。このことから、本来一度に行う作業を2度に分けて作業しても、既存の CPU と大差無い ( 遅くはならない ) と判断したのでしょう。
たしかに、実行ユニットを多く搭載すれば、同時に複数こなせます。反面、消費電力が増えてしまいます。モバイル専用に省電力な製品として開発を進める以上、実行ユニットを増やさなかったと想像できます・・・
※ Pentium M の後発にあたるCore 2 Duo / Quad や Core i7 では並列に実行されるように改良されています。

新機能を使うようなコードでもそうでないコードで書いても、結果がほぼ同じ。
新機能の命令を受け入れる・理解することはできるが、内部での実行手順が今までと異なる。期待したほど高速化されない環境もあるということだ・・・

ほかにも、似た例として、Core 2 Duo / Quad と 64ビット命令を実行した際の状況が挙げられる。中には32ビット時より落ちてしまうといった記述も見掛けるが、64ビット環境下では本来の実力を発揮できないと表現したほうが近い。
その理由としては、Core 2 Duo / Quad は 32ビット版 CPU としての動作に向けて高度な最適化が加えられていたのに比べ、64ビット版命令対応を急いで加えた感がある。とりあえず動くことが優先されたのだろう。もっとも、Core 2 Duo / Quad が登場したての頃、64ビット版アプリはほとんど無かった。

これを書いていた時点でPC に搭載されている CPU は Core i7 ( Ivy bridge )。Ivy bridge の ひと世代前の CPU ( Sandy bridge ) から Intel AVX 命令が実行できるように拡張された。AVX 命令は256ビット幅の YMM レジスタを扱うことがでる。が、Sandy bridge や Ivy bridge では128ビット分ずつ2回に分けて処理している。これも新機能の命令を受け入れることはできるが実行手順は最適化されていない例。そのため、性能をフルに発揮できない。ちなみに、次世代の Haswell ではこの辺が改良され1回で処理できるとアナウンスされています。

ということで、高速化したい部分を新機能を活かすコードへの書き換えるのはひとつの策です。しかし、将来、ハードウェアレベルで改善されるかもしれません。こまめにハードウェアの先行情報、ロードマップをチェックすることで、その書き換え作業が無駄にならないかの判断材料になることでしょう・・・

C/C++ 言語等を用いたアプリを作るとして、全てのヒトが実行環境の特徴を意識する必要はありません。手の込んだコードや特定の機材や環境に絞ったコードに書き換えてしまうと、汎用性が損われます。
Windows 以外の OS や、違うCPU を搭載したマシンといった異なる環境への移植を考えた場合、足かせとなります。かつての作品のこの部分や考え方を他で活用したくなることは多々あります。その際、機種依存で書いた部分は壁となります。
移植する先にその機能が備わっていない場合、ソフトウェアレベルで補うようなルーティン ( 関数 ) を新たに設けるなど面倒が増えます。
逆に、特定の機材と環境に絞れる状況かつ移植も考えないのであれば、C/C++ 言語等ではなく機械語で書いてしまうのが良いでしょう・・・

そろそろ、整数の除算の話に入りたいところですが、
長くなりましたので今回はこの辺で・・・

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

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

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

[未掲載分] 「除算が遅い」の補足 (6)

アプリを作る際、レジスタの本数が足りず演算結果を一旦メモリ上に保存したり、読み込み直すことが速度向上の妨げとなる。「除算が遅い」の補足 (3)で綴ったように、32ビット版のアプリを組む際に顕著である。
「除算が遅い」の補足 (4)「除算が遅い」の補足 (5)では、お手軽に新しく追加された機能を活かすアプリを作る方法を触れた。
しかし、お手軽な方では、期待したほど高速化されないケースも生じる。理想的な命令とは異なる命令を用いた実行コードが生成されるケースも考えられる。

XMM レジスタの幅は128ビット分。C/C ++言語で言えば、double 型の数値を2つ分、float 型の数値を4つ分同時に扱える。ゆえに、その分高速化されるハズ。ところが、同時に複数の加算や乗算が行われないような実行コードに翻訳されてしまうこともある。この辺はソースコードの組み方だけでなく、コンパイラの賢さにも左右される。もちろん、コンパイラのバージョンが上がる毎に改善されている。

前話で述べたように、高速化をコンパイラに任せる副作用として、要件を満たさない環境で実行した際のトラブルも希に発生する。
そこで、今回は
/arch:SSE2 オプションを適用しない。
高速化したい箇所のみ新機能を活かすようなソースコードに書き換え
について・・・


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


実数 ( 浮動小数点数 ) と整数で状況は異なります。今回は、お手軽ではない方の浮動小数を中心としたお話。

C/C++ 言語で浮動小数を扱いたい場合、一般的には float や double といった変数の型を用いる。倍精度浮動小数点数型のdouble 型 を軸に・・・ double 型の数値は64ビット幅うんぬん・・・と文字では判り難いヒトもいることでしょう。簡単な図で示します。

まずは FPU と 倍精度浮動小数点数。



FPU ( 浮動小数点演算処理装置 ) は 80ビット幅の数値まで扱えます。
西暦1993年頃から、CPU にFPU が標準搭載された。それまで、FPU はオプション扱いで、搭載されていない PC も多かった。
今後、FPU は廃止へと向かっています。古くからの書籍やWeb サイトでは FPU を前提とした記事が載っているのでは・・・

次に、(SSE 登場時より追加された) XMM レジスタでのイメージを図で表すと下のよう。



レジスタの幅が128ビットあり、64ビット幅の数値であれば2つ分を扱える。命令ひとつで複数の演算が可能となっている。FPU が除算を1回行うのに比べ、XMM レジスタ内の2組分の除算を行えるのがメリット。クロック数、つまり、待ち時間はほぼ一緒。
※ 一部のCPUを除く。

簡単に言えば、トラックの荷台が広くなり、今までの倍の荷物を積めるようになったようなもの・・・

さて、XMM レジスタが下図のような状況もありうる。



左半分のグレー「未使用」と記した部分は、「使われない」「利用されない」という意味。

冒頭に述べた理想的な命令とは異なる命令に翻訳された場合はこのような状況。XMM レジスタのうち、下位の半分しか「利用されない」。ひとつの命令で同時に2つの演算ではなく、ひとつの演算しか行わない。
原因としては、連続的に配置されていない、ハードウェア的に備わっていない機能を補うため ( 三角関数に関するライブラリを呼び出す ) などが挙げられる。

簡単に言えば、トラックの荷台がまだ半分空いているのに出発するようなもの・・・



高速さの話と反してしまうが、不慣れなヒト向けに少し補足。
演算結果の一貫性、つまり、精度に関して頭に入れておいたほうが良い。
大雑把な言い方をすれば、正しい値からのズレをどの程度妥協できるか・・・

FPU は 80ビット幅の数値まで扱えます。
Visual Studio で作成したアプリにおいて、double 型の数値は64ビット幅としてメモリ上に置かれたり、ファイルに書き込まれる。これらは読み出された後、FPU の中で80ビットの精度で演算が行われる。演算結果を FPU からメモリに書き出す際には64ビット幅の値に丸められる。
一方、XMM レジスタでは64ビット幅の倍精度のまま演算が行われる。もろん、ビット数が高い (桁数が多い) ほうが、結果は誤差が少なくなる。誤差という表記は混乱するかもしれません。

コンピュータは正しい結果を示すと思い込んでいるヒトが多い。
たいていの PC において浮動小数点数の計算は、正確な答えに一番近い数を探し出しているに過ぎません。
誤解が生じないように加えると、PC のはじき出した数字が信用できないということではありません。正確さが重視されるアプリ等は、人間からみてより正確な答えに近づくよう補正する仕組みを備えています。

人間の目線で考えて「1.0」が正しい答えであるとしても、途中で誤差が生じて
「0.99999999~~~」や「1.0000 ~~~たくさんのゼロ ~~~ 03」等の答えが返ってくることもあります。これはバグや欠陥ではありません。

例えば「A ÷ B × ○△□ × C」といった式において、変数 A、B、C の内容は実行時まで不明だとします。実行時にA が 1.0 、B が 3.0 、C が 3.0 の数が代入されたならば、
「1.0 ÷ 3.0 × ○△□ × 3.0」となります。

人間の場合、分数の概念があるので「1.0 ÷ 3.0」は3分の1、さらにそれを3倍して通分して~~~と考えることができます。最終的には A、B、C 以外の部分を計算した結果と答えが等しいと判断できます。
一方、PC の内部では分数ではなく、「1.0 ÷ 3.0」の部分は「0.33333333~~~」といった割り切れない数として扱われます。よって、末尾の「× 3.0」で3倍して「0.99999999~~~」等の数値が返ってきます。
先ほど「正しい答え」ではなく「正確な答えに一番近い数」と記したのはこのようなケースがあるから。

しばしば、高い精度のほうが好まれる。それは割り切れない数などから生じる誤差をなるべく少なくしたいからである。突き詰めれば、より正確な値を扱う手法もあるのだが高速ではない。
とはいえ、画像や音を処理に関して言えば、よほど敏いヒトでないかぎり感じないレベル。
高速化を目指すなら、ある程度の近い値で妥協するといった割り切りも必要である・・・



例として、
A = B × C ÷ D を10000回繰り返すとしましょう。流れとしては、ロード・演算・ストアのひたすら繰り返し。

過去記事除算が遅いなどで
・浮動小数点の除算ならば、「逆数の掛け算」に置き換えると良いと綴りました。

A = B × C ÷ D は
A = B × ( C ÷ D ) と同じ。
基のB は毎回メモリから読み出すため異なる値、C、D の値が決まった値、つまり固定としよう。( C ÷ D ) の部分を予め求めておく。それを E = C ÷ D と置き換えることにより、演算の部分 は A = B × E となる。単純に見て、演算のステップがひとつ減る。
ちょっと判り難いかも。何らかの数を当てはめてみれば判り易くなるだろうか。
A = B × C ÷ D の C は 4、D は 5としよう。
A = B × 4 ÷ 5となる。
A = B × 0.8
・・・といった具合に、除数 ( 割るほうの数 ) が変わらないならば、遅くなりがちな除算を避けることも可能。

今回は意図的に除算を避けない形で話を進めます。さらに、前提条件として、基のBは 10000個の値が連続的に配置されているものとします。

高速さを求めるならば、
読み出す値がどのように格納されているかも重要なカギ。



連続的に配置されているならば、データを読み出しがスムーズ。
そうでない場合、先に述べた XMM レジスタの下位半分しか使わない命令に翻訳される確率が高くなります。

簡単に言えば、何かを作ろうとして原料・材料があちこちバラバラに散っている場合よりも、ひとまとめに揃えてある方が速やかに捗るというのと同じ。
もし、基となる数値がバラバラに散っているのであれば、その辺の見直しすだけでも効果があります。
演算ループに入る前に暫定的なメモリ空間を確保し、基の数値を連続的して配置。つまり、一列に整列させてから演算ループに入ると良いでしょう。ループから脱したら暫定的なメモリ空間の解放をお忘れなく。
メモリ空間を確保や解放については、過去記事車輪の再発明 (6)車輪の再発明 (8)で触れていますので省きます。
また、それらの過去記事で触れたように、メモリ空間のアドレスが16の倍数に揃っていれば若干速度が向上します・・・

予め記しておきます。汎用レジスタが対象の場合、メモリからデータを「ロード」、結果をメモリに格納する際には「ストア」の語を用います。FPU に対する操作は 「ロード」、「ストア」ではなく「プッシュ ( 積む )」「ポップ 」を用います。Push の反対はPullではないかとの声も聞こえてきそうですが・・・

A = B × C ÷ D を10000回繰り返す
まずは、旧来からのFPU ( 浮動小数点演算処理装置 ) を経由して演算を繰り返すとして
(A-1) 64ビット幅の浮動小数Bをスタックへプッシュ
(A-2) FPU のスタックの値とCを掛け
(A-3) スタックにある値をとDで割り
(A-4) FPU スタックの値をメモリへポップ
(A-5) 浮動小数Bが格納されているポインタを64ビット幅の浮動小数ひとつ分後方へ・・・
といった感じでしょうか。 (A-1)から(A-4) を10000 回繰り返します。

次に、XMM レジスタを使った、つまり、新しい機能を活かした流れは
(B-1) 64ビット幅の浮動小数Cを2組 XMM レジスタ(0) へロード
(B-2) 64ビット幅の浮動小数Dを2組 XMM レジスタ(1) へロード
(B-3-1) 64ビット幅の浮動小数Bをふたつ、B[0] と B[1] を XMM レジスタ(2) へロード
(B-3-2) XMM レジスタ(2) と XMM レジスタ(0)を掛け
(B-3-3) XMM レジスタ(2) と XMM レジスタ(1)で割り
(B-3-4) XMM レジスタ(2) の値をメモリへストア
(B-3-5) 浮動小数Bが格納されているポインタを64ビット幅の浮動小数ふたつ分後方へ・・・
といった感じで (B-3-1)から(B-3-5) を5000 回繰り返す。

10000回繰り返すのでは!?!?!?
と誤解するヒトがいるかもしれませんので補足。
一見、(A-2) と (B-3-2)、(A-3) と (B-3-3) が同じように見えます。
前者は一度に一組の乗算や除算、後者は一度に二組の乗算や除算を行います。同等のクロック数 ( 待ち時間 ) で2組分の除算が行える点が後者のメリット、アドバンテージ。

流れ (A)、(B) に即したコードを比べてみましょう。まず、流れ (A)
for(i = 0 ; i < 10000 ; i += 1){
double b = *p;// (A-2)
b *= c;// (A-3)
b /= d;// (A-4)
p += 1;// (A-5)
・・・のようになる。
簡単に説明すると、変数 C、D は for(分の前に値を代入、変数 i は ループ回数、変数 p は浮動小数Bを読み出す位置、つまりポインタ。

次に、流れ (B) に従い SSE2 命令を使うように変更を加えるならば、
__m128d xmm0 = _mm_set1_pd(4.0);// (B-1)
__m128d xmm1 = _mm_set1_pd(5.0);// (B-2)
for(i = 0 ; i < 10000 ; i += 2){
__m128d xmm2 = _mm_loadu_pd( p );// (B-3-1)
xmm2 = _mm_mul_pd(xmm2 , xmm0);// (B-3-2)
xmm2 = _mm_div_pd(xmm2 , xmm1);// (B-3-3)
p += 2;//(B-3-5)
・・・のような感じ。

__m128d データ型の部分は、2組のdouble 型をひとつに扱うための変更。流れ(A) と間違わないように変数名を xmm ~と付けてあります。
ほか、パッと見、_mm_ ○△□ _pdがたくさん!?!?!?
この辺の読み方は過去記事車輪の再発明 (8)で触れています。
頭に「_mm_」が付くのが共通として、
○△□ の部分が掛け算なら MUL (Multiply の略) 、 割り算ならDIV ( Divide の略 )、
末尾「_pd」は同時に2組の64ビット幅の数値演算を行うことを意味する。ちなみに、末尾が「_sd」ならば1組の64ビット幅しか扱わない、つまり、XMM レジスタのうち下位の半分を使うという命令。



数値をロードする部分は _mm_loadu_pd _mm_load_pd の選択肢があります。
loadの後ろに「u」が付く・付かないの違いがあります。
前者は16バイトの倍数以外のアドレスからデータを読み出せますが、後者は16バイトの倍数以外のアドレスからデータを読み出そうとすると例外が発生し、アプリの不正終了へとつながります。
データを読み出すアドレスが16バイトの倍数に整っているのが不確実なので前者を用いました。データのアドレスが16バイトの倍数であることが確実ならば、後者に切り替えることで若干速度が向上します。

たしかに、_mm_ ○△□ _pdを覚えるのは面倒。Visual Studio にはそれらを軟らげる便利なクラスが用意されている。dvec.h や fvec.h 等のヘッダーファイルをインクルードすることで便利なクラスを使うことができる。
なかでも __m128d 型 に対応するのはF64vec2 クラス。Float , 64ビット幅、組分の略と考えれば覚え易いでしょう。
これを用いれば
F64vec2 xmm0(4.0 , 4.0);// (B-1)
F64vec2 xmm1(5.0 , 5.0); // (B-2)

F64vec2 xmm2(*(double *) p);// (B-3-1)
xmm2 *= xmm0;// (B-3-2)
xmm2 /= xmm1;// (B-3-3)
・・・のような感じ。
_mm_ ○△□ _pdなどのコンパイラ組み込み関数を用いる場合に比べ、流れ(A) から変更箇所が少なく済む・・・


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

まだまだ、速度向上の余地がありそう。「除算が遅い」の補足 (4)で取り上げた
並列に実行できる、できない
などを練り直すことで、さらなる速度向上を狙えるかも!!!

32ビット環境では 8本、64ビット環境では 16本 の XMM レジスタが備わっています。XMM レジスタをさらに2組使うとして

(C-1-1) 64ビット幅の浮動小数Cを2組 XMM レジスタ(0) へロード
(C-1-2) 64ビット幅の浮動小数Cを2組 XMM レジスタ(1) へロード
(C-2-1) 64ビット幅の浮動小数Dを2組 XMM レジスタ(2) へロード
(C-2-2) 64ビット幅の浮動小数Dを2組 XMM レジスタ(3) へロード
(C-3-1-1) 64ビット幅の浮動小数Bをふたつ、B[0] と B[1] を XMM レジスタ(4) へロード
(C-3-1-2) 64ビット幅の浮動小数Bをふたつ、B[2] と B[3] を XMM レジスタ(5) へロード
(C-3-2-1) XMM レジスタ(4) と XMM レジスタ(0)を掛け
(C-3-2-1) XMM レジスタ(5) と XMM レジスタ(1)を掛け
(C-3-3-1) XMM レジスタ(4) と XMM レジスタ(2)で割り
(C-3-2-1) XMM レジスタ(5) と XMM レジスタ(3)を掛け
(C-3-4-1) XMM レジスタ(4) の値をメモリへストア
(C-3-4-1) XMM レジスタ(5) の値をメモリへストア
(C-3-5) 浮動小数Bが格納されているポインタを64ビット幅の浮動小数 4つ分後方へ・・・
といった感じにすれば (C-3-1-1)から(C-3-5) は 2500回となります。演算を繰り返す回数も 1/4。
ひとつ前の実行結果を待つ必要が無い部分は並列に実行されるハズ。

理想としては、当初の FPU を経由した演算に比べ4倍に向上してほしいところです。
「なぜ4倍なの!?!?!?」と疑問に感じるヒトもいることでしょう。トラックの荷台で言えば、荷台の広さが2倍、利用できるトラックの台数も2倍に増えたのに等しいから。

実際は、並列処理を手助けする回路に左右されます。昨今の CPU では 実行ユニットを複数備えています。
実行ユニットの数が多いほど並列処理も速やか。体感から言えば、これを書いている時点の CPU では 2~3命令が並列に実行されているようです。
単純に4倍の速度向上には至らないまでも、当初に比べ 2.5 ~ 3 倍は速くなるのでは・・・

細かいことを言えば、XMM レジスタは並列処理が得意な反面、EAX ~ EDX 等の汎用レジスタに比べロードやストアに比べ3 ~ 5 倍のクロック数 ( 待ち時間 )。
※ なお、これを下書きしたのは2012年頃。ご覧いただいている時点では CPU の進化とともに、ロード、ストアは遅さは解消されているかもしれません。

かつてはこの辺を人間の手で調整( ロードや演算の手順を修正 ) することでさらなるパフォーマンス改善も期待できた。同時に、ソースコードが曲芸的に偏りがちで、読み難くなる傾向にあった。
昨今は CPU の進化にともない、メモリアクセスの効率化、先に実行できる命令か否かなどを能動的に判断するような仕組み等も強化されている。
後日の保守作業を想定するならば、曲芸的なコードを書くことは避けたいものである・・・

長くなりましたので今回はこの辺で・・・

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

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

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

[未掲載分] 「除算が遅い」の補足 (5)

前話の終盤、拡張された機能を有効にするお手軽な方法を挙げた。Microsoft の Visual Studio でアプリを作成する際、



/arch:SSE2 オプションを指定することにより、ある程度アプリが高速化される。
西暦1999年頃リリースされたPentium III 以降、CPU に XMM レジスタが増設された。つまり、新機能が追加された。新機能を活かすことで高速化に繋がる。アプリをビルドする際、初期設定のままでは「新機能を積極的に利用する」にならない。

なぜ、/arch:SSE2 オプション/arch:AVXが選択制なのだろうか???
デフォルトで有効に設定されても良いような感じもするのだが・・・

※ 「デフォルト」は財政破綻、債務不履行等の意味で用いられることもあるが、ここでは「初期設定」や「標準の状態」を指しています。


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


※ これを下書きしたのは2012年夏の終わり頃です。
ご覧いただいている時期によっては状況が異なっている可能性があります。
下書きした頃の環境は、OS - Microsoft Windows 7、開発環境 - Visual Studio 2008 , C/C++ 言語。

新機能を活かすことで高速化に繋がるのは良いとして、
・ハードウェアレベル、ソフトウェアレベルで対応しているかの確認
を見落としがちである。

旧世代のCPUにはその機能が備わっていない。また、将来的にその機能が廃止されるかもしれない。実例を挙げると、32ビット環境で永い間用いられてきた FPU ( 浮動小数点演算処理装置 ) や MMX が廃止の方向にあり、64ビット版の Windows ではサポート対象外となった件などなど・・・

SSE ( SSE2 から SSE4.2 ) や AVX 命令など後からを追加された機能を活用したアプリを作る場合、
PC にその機能が備わっているか、OS がサポートしているか等、
ハード・ソフトともに対応しているかのチェックを怠らないことが重要。具体的には、
・CPU にその機能が実装されている
・OS が公式に対応している
を確認する。それらの条件を満たしていないとアプリが不正終了したり、OS がフリーズや暴走する原因となる。

とはいえ、自分のマシンで自分のみ使うようなアプリならばこのような確認も不要・・・

アプリを作る側の環境は固定できるが、利用する側は様々である。作る側が「動作環境として SSE3 以降」や 「AVX 機能搭載機種」など細かく書いたとしても、利用する側がそれを理解できるとは限らない。
そこで、ハード、ソフトどちらかが対応していないならば、「要件を満たしていない」等のダイアログボックスを表示し、アプリを終了するなどの策を施すのが良い。利用する側に親切である。

具体的にはWinMain もしくは tWinMain 等アプリのメイン関数冒頭にハード、ソフトをチェックするコードを加える。
ハードウェアレベルでサポートされているか調べるには CPUID 命令 を使う。



戻り値のうち、特定のビットが立っているか否かを調べることでその機能を有しているか判別できる。詳細は
・IA-32 インテル アーキテクチャ ソフトウェア・デベロッパーズ・マニュアル
・インテル エクステンデッド・メモリ64 テクノロジ・ソフトウェア・デベロッパーズ・ガイド等に載っています。それら資料の入手に関しては過去記事車輪の再発明 (8)で触れました。

1993年頃登場した Pentium 以降の CPU ならば CPUID 命令が通用する。古めの Visual Studio ではCPUID 命令に相当する関数が付属していなかった。そのため、アセンブリ言語 ( インラインアセンブラ ) で CPUID 命令に相当する部分を書く必要があった。
Visual Studio 2005 以降であれば、 _cpuid 関数 などの組み込み関数が用意されている。

ハードウェア寄りな話が苦手なヒトもいることだろう。たしかに、CPUID 命令の発行やどのビットを調べれば良いかといった部分が面倒かもしれない。かつて、車輪の再発明 (10)でも触れた方法も使える。Windows に限られてしまうが、IsProcessorFeaturePresent 関数 を呼び出して、戻り値を調べる方法。



この関数は、OS が標準装備している機能を呼び出す。よって、Visual Studio のバージョンに左右されない。
※ 1996年頃に登場した Windows NT 4.0 以降で標準装備されている。

実行環境が SSE2 命令に対応しているかを調べるには PF_XMMI64_INSTRUCTIONS_AVAILABLE を指定する。
PF_△△△_AVAILABLE や PF_■■■_ENABLED といった引数 ( 関数を呼び出す時に指定する値 ) は Windows SDK のバージョンが上がる毎に新しい CPU に対応した引数が追加されてゆく。うまくコンパイルできない時はWinNT.h 等のヘッダーファイルを開くこき確認すると良い。



例えば、SSE3 命令に対応しているかを確認したいとしよう。IsProcessorFeaturePresent 関数 を呼び出す部分で PF_SSE3_INSTRUCTIONS_AVAILABLE を指定する。
Visual Studio 2005 をセットアップした段階で付属している Winbase.h や WinNt.h を開いてみると PF_SSE3_INSTRUCTIONS_AVAILABLE が含まれていない。

Visual Studio とは別に Windows SDK (Platform SDK や Win32 SDK とも呼ばれていた) が公開されている。新しいバージョンのWindows が登場するとそれに対応した SDK がリリースされる。確認したところ、西暦2008年にリリースされた Windows SDK 6.1 ( Windows SDK for Windows Server 2008 and .NET Framework 3.5 ) に含まれているヘッダーファイルから PF_SSE3_INSTRUCTIONS_AVAILABLE の定義が加っている。



ということで、Visual Studio 2005 に Windows SDK 6.1 ( もしくはそれより新しいバージョンのSDK ) を導入することでSSE3 判定用の引数、PF_SSE3_INSTRUCTIONS_AVAILABLE を使える。参考までに加えておくが、Visual Studio 2005 と Windows SDK 7.0 以降の SDK は相性が悪い部分がある・・・

つぎに重要なことはソフトウェアレベル、つまり OS がサポートしているか。細かく言えば、
OS がアプリの実行権を切り替える際、各レジスタの退避・復元を確実に行うか否か
がサポートされるか否かの境目である。SSE2 に絞って言えば、XMM レジスタの退避・復元が確実に行われれば良いことになる。

これに関しては、 _cpuid 関数IsProcessorFeaturePresent 関数のようなOK か NG かをお手軽に判別する関数が用意されていない。おそらく、実行時、どのバージョンの Windows で実行されているかを判断し、得られたバージョン情報にもとづいて続行か終了かを決めるようなコードを自力で書くことになる。

これを書いていた時点で、
OS のバージョンを取得するにはGetVersionEx 関数 VerifyVersionInfo 関数を用いるのが簡単である。これら関数を用いることで現在動作している Windows のバージョンに関する情報を得ることができる。得られたメジャー番号、マイナー番号やサービスパックの適用情報をもとに、動作対象か否かを判別できるハズ。
ほかにも、OS のバージョンを取得するにはGetFileVersionInfo 関数でシステム DLL のバージョンをチェックする手もあるのだが・・・
なお、これらの関数は 古くから ( Windows 95 や NT 3.5 の頃から ) のものであり、これをご覧いただいている時点では非推奨や廃止となっているかもしれません。その場合、代替の関数が提供されてることでしょう・・・

Windows のバージョンと対応できる拡張機能を判定する部分を自力で書く際には、同じバージョンの Windows であってもマイナーバージョンが違えばサポート外な件もあるので注意したい。
例えば、これを書いていた時点でPC に搭載されている CPU は Core i7 ( Ivy bridge )。Ivy bridge の ひと世代前の CPU ( Sandy bridge ) から Intel AVX 命令が実行できるように拡張された。
もともと、SSE4.2 の後継として SSE5 に注目が集まっていた。しかし、レジスタの幅が128ビットから256ビットなどの仕様が変更された AVX が採用された。
2012年の時点で Windows 7 と Windows 8 が流通している。AVX 機能を活用したアプリを作りたいとして、OS が Windows 7 であっても「Microsoft Windows 7 Service Pack 1」(SP1) が適用されていれば可能である。いわゆる無印、発売当初の Windows 7 では AVX 機能はサポート外だった。
AVX から扱えるようになった256ビット幅 のレジスタは YMM レジスタと呼ばれ、そのうち下位128ビットは SSE の頃からの XMM レジスタと共用。初期の Windows 7 では YMM レジスタ のうち上位 128ビット分の退避・復元が正しく行われないのがサポート外の原因とされている・・・

時を遡って、SSE がサポートされたのは Windows 98 からであって Windows 95 ではサポート外であった・・・

どのバージョンの OS からどの機能が使えるかを簡単に書いておくと、
SSE や SSE2 ならば Windows 98 以降
AVX ならばSP1 適用済の Windows 7 や Windows 8 以降
となる。

さらに、MSDN等で/arch:SSE2 オプションの項目を眺めると、
コンパイラは ~~~ その他の命令も使用 ~~~ 例としては ~~~ CMOV 命令 ~~~
との記述がある。ここも見落としがち・・・

そもそも、CMOV 命令とは何ぞや!?!?
と感じるヒトもいることだろう。簡単に言えば、「条件分岐」を迅速に処理するために追加された命令。

比較して、条件成立時は△△、そうでない場合は ■■ を代入したいとする。C/C++言語を想定するなら
if (条件式) 変数 = △△; else 変数 = ■■;
もしくは
変数 = (条件式)?△△:■■;
と書く。両者とも同じ意味である。一般的に、if 文と三項演算子どちらで書いても同じマシン語コードが生成される。
後者の三項演算子で書いた方がスマートで楽なのだが、可読性で不利。他のヒトが読む場合や、書いてからある程度時間が経過した頃に自身でメンテナンスを行う際、即座にコードの意味を把握できないかもしれない。不慣れなヒトが三項演算子で書かれたコードに直面した際、戸惑ってしまうことだろう・・・

「条件分岐」を機械が実行する流れは以下のような感じ。
(1) 条件を比較
(2) 条件が成立しないならば (5) へジャンプ。
(3) 変数 に △△ を代入。
(4) (6) へジャンプ。
(5) 変数 に ■■ を代入。
(6) それ以降の命令・・・

機械の中ではこのように 5 段階の処理が行われる。(2) と (4) でジャンプが行われる。「ジャンプ」と記した部分は飛び跳ねるではなく、「あっちへ行け、こっちへ戻れ」といった意味である。
なぜ、この辺りが速い遅いに関わってくるのか判り難いだろうか。例えば、実生活の場において、いつもの通り道が工事中で渋滞しているとしよう。迂回して遠回りすればいつもより遅くなる。
古くから「比較命令と条件分岐が遅くなる原因」と言われてきた。そこで、分岐を減らすために追加されたのが CMOV 命令である。

いっとき、CMOV 命令を使うか否かで数倍の速度差があると言われていた。昨今、CPU の進化にともない「比較命令と条件分岐命令の組み合わせ」でもそこそこ速やかに実行されるようになってきている。それよりも、ここで頭に入れておきたいのは、CMOV 命令の速さではなく、CMOV 命令の互換に関してである。

x86 と呼ばれる Intel の i386 ( 80386 ) の流れを汲む CPU と互換プロセッサーが存在する。ここでの「互換」とは「同じ命令体系を理解実行できる」という意味。かつては物理的にも互換性があり、Intel 製の CPU ソケットに他社製の CPU を装着可能な時期もあった。現在では物理的な互換性は無い。
互換プロセッサーとしてメジャーなのはAMD の Athlon や Phenom シリーズである。ほかにもマイナーな互換プロセッサーが存在する。マイナーな互換プロセッサー の中には SSE 命令に対応しているがCMOV 命令に対応していないモノがある。
Intel や AMD に絞って言えば、SSE 命令に対応している CPU は CMOV 命令にも対応している。だからといって、「SSE 命令対応だからCMOV 命令も使える」と決め付けるべきではない。

/arch:SSE2 オプションでは自動的にCMOV 命令も使われる。残念ながら「SSE2 命令を使うがCMOV 命令は使わない」という選択肢はない。したがって、より丁寧なアプリを目指すならば、アプリのメイン関数冒頭で
・SSE2 命令に対応しているか?
のほかに
・CMOV 命令も備っているか?
を確認するのコードを加えると良い。そうすれば、CMOV 命令に対応していない互換プロセッサーで実行した際の未定義な命令、無効な命令などで異常終了やOS のフリーズを避けることができる。「正常に動く」「同じ質を保つ」がアタリマエ。高速化される代償として不正終了等が発生するようなアプリとなってしまうのは本末転倒・・・

CMOV 命令が扱えるかの確認はIsProcessorFeaturePresent 関数ではなく、 _cpuid 関数 やCPUID 命令に頼ることになる。これを書いている時点で PF_△△△_AVAILABLE を眺めるかぎり CMOV に該当する定数が含まれていない。

さてさて、書籍や Web サイトを眺めていると
拡張された機能を使うことで以前よりも数倍速くなった
といった旨の記述も多く魅かれる。しかし、不慣れなヒトには少し難しい内容かも。それに比べお手軽な
拡張された機能を有効にするだけでは期待したほど高速化されないかもしれない。

古くは、Microsoft Visual Studio 6.0 と Visual C++ Toolkit 2003 を組み合わせでも/arch:SSE2 オプションを指定できた。コンパイラのバージョンが上がるごとに良好なコードが生成されるように改良されている。とはいえ、劇的な高速化は望めない。
Microsoft 以外のコンパイラではさらなる最適化を施すような製品も販売されている。CPU を製造している Intel からもコンパイラがリリースされている。それに比べ Microsoft の Visual Studio 付属のコンパイラだと高速化の対象はスカラ演算に限られてしまう。
そこで、コンパイラ任せではなく、明示的にSSE2関連の組み込み関数使って最適化を目指すという選択肢もある。
つまり、/arch:SSE2 オプションを指定するのではなく、
高速化したい部分のみ拡張機能を活かすコードに書き換える
手法を試みたくなるものだ・・・

長くなりましたので今回はこの辺で・・・

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

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

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

[未掲載分] 「除算が遅い」の補足 (4)

今回も「除算が遅い」、「除算の高速化」などのキーワードで検索サイトからお越しいただいている方々への対応記事となります。

前話で、
レジスタ数に余裕があるか否かでアプリの実行効率が変わってくる
旨を述べた。

レジスタの本数うんぬんという例えでは判り難いというヒトもいることだろう。
作業人数が足りているか人手不足かといったところだ。

例えば、何かの製造作業に没頭しているとしよう。ほかの部門で人手不足が生じたので手伝うようにと指令された場合、現在行っている作業を中断してほかの部門の手伝いに向かう。
やがて、お手伝いが終わったところで本来の作業に戻る。然るに、お手伝いに関わっていた分、その前に関わっていた作業は遅れてしまう。

今回は除算そのものの話ではなく、前話の続き・・・


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

空きレジスタうんぬんといった話し入る前に頭の体操を。

依存関係を考えずにアプリの高速化を考えると遠回りである。
並列に実行できる、できない
を練ることで問題解消のカギとなるハズ。

まず、単純な2つの式で考えてみよう。





それぞれは独立している、つまり
依存関係が無い。
もし、2番目の式が D=E+F ではなく D=A + F ならば、B÷C の結果が出るのを待ってから A + F に着手することになる。

別の表現を用いれば、
作業を分担できるか否か
である。前後の作業に関連性が無い作業なら同時刻に実行しても差し支えない。

昨今のCPU は複数の実行ユニットを備えている。前後の命令が独立したものであれば、複数の命令を同時に実行できる。
アプリを作ってみたいと思い立ち、書籍やWebサイトに載っているソースコードを打ち込んで満足してしまうヒトもいることだろう。
綺麗なソースコードを学ぶだけでなく、ハードウェアの特性を理解しながら進むことで良い結果が導き出されるかも・・・

さてさて、本題の空きレジスタうんぬんの話。
前話でも述べた通り、64ビット版の CPUでは汎用レジスタの幅が64ビットに拡張されたほか、本数も倍の16本に増えた。
64ビット版の CPU が登場する前、1999年ごろ登場した Pentium iii で XMM レジスタが8本追加された。XMM レジスタといってもピンと来ないヒトもいるだろう。SSE 命令を扱うための専用レジスタである。

XMM レジスタはベクトル演算を得意としており、汎用レジスタの4つ分にあたる128ビット幅。昨今さらに上位128ビット分が拡張され、256ビット幅を持つ YMM レジスタが拡張されている。XMM レジスタが SSE 命令を扱うために増強されたとすれば、YMM レジスタは AVX 命令を扱うための増強である・・・

ところで、その前にレジスタが追加されていたのではないかと疑問に思うヒトもいるだろうから補足。
1990年代、PC で 音声や動画を楽しみたいという声が高まった。マルチメディア処理を円滑にとの目的で1997年頃、MMX 命令を処理する機能が追加された CPU が登場した。MMX 命令対応の Pentium と後継機の Pentium II がリリースされた。前者はマザーボードが対応していれば、今までのPentium と差し替えて使うことができた CPU である。
後者は物理的な互換性が無く、某家庭用ゲーム機のゲームカートリッジのような形状をしていた、それまでの CPU 装着部がソケットなのに対し、スロットと呼ばれた。



初期の Pentium iii もこの形状。
スロット形CPUを装着するにはコツがあり、PC 関連の雑誌でも取り上げられていました。この頃、PC の自作が流行しいましたが、CPU が奥深くまで挿さっていない等でPC が起動しないといったトラブルに遭ったヒトもいるようです。



また、スロット形CPUはソケット状のCPUと比べ大型で、PC ケース内の通気性という面で不利でした。後からソケット状の Pentium iii がリリースされ、一般の職場や家庭向けのデスクトップ PC ではスロット形CPUを見かけなくなりました・・・

MMX 命令を実行できるように拡張された際、64ビット幅のレジスタが8本増設された!?!?
といった誤解のもととなる記述も見受けられます。実際のところ、増設ではなく転用。

1993年頃登場した、Pentium から FPU ( 浮動小数点演算処理装置 ) が標準搭載されました。FPU はコプロセッサや NDP 、NPX とも呼ばれていましたが、文字通りの演算専用の装置。それまでは後付けであったり、廉価版では省かれていました。
特定の科学分野等を除き、FPU はほとんど活用されていませんでした。それを活かす方向で MMX 命令を実行できるように施されました。
FPU は80ビット幅のスタック8本で構成されています。これを64ビット幅の整数演算レジスタ8本として割り当てられたのです。つまり、専用レジスタの新設ではなく既存リソースの転用です。

裏を返せば、FPU と MMX は排他的に利用せねばなりません。浮動小数点演算とMMX 命令は同時に実行できないほか、当初は切り替え手続きも迅速ではありませんでした。
待ち時間は汎用レジスタに数値をロードやストアする命令の数十倍とされていました。

排他的や切り替えといった表現は難しいかも。一人二役でライブに出演するような事を想定してみましょう。最初の役を演じ終り次の役へ移る際、衣装を着替えたりメイクを変えるなどの準備に手間取ってしまう。その分周囲は待たされてしまうようなものです。
後々CPU が進化するにつれ、FPU と MMX の切り替えに要する待ち時間も短縮されました。昨今のCPU では データのロードやストアの待ち時間とほぼ同等で切り替えることが可能です。

64ビット版の Windows では FPU や MMX がサポートされていません。
誤解の無いように加えておくと、ほかのアプリと実行権を切り替える際、現状の値を保存、復元する仕組みが削減されたのが原因と言われています。
マルチタスクの環境において、複数のアプリが同時に起動してます。アプリの実行権切り替えは OS が行います。アプリの実行権が一時ほかに移り、次に実行権が巡ってきた時にレジスタやスタックの内容が一致する保障されません。
64ビット版の Windows で 32ビット版を実行する際には保存、復元する仕組みが働くように工夫が施されています。過去に作成されたアプリやソースコードを活用できるようにとの配慮かもしれません。
この辺から、FPU や MMX がハードウェア的に動いているのが判ります。そのことから、64ビット版のアプリを組む際、レジスタやスタック操作の命令を加えることも可能ではないかと思えてきます。
独自にゴニョゴニョと試したところ、64ビット版の Windows で 64ビット版を実行する際でも FPU や MMX の内容を保存、復元する仕組みは残されていました。
とはいえ、時期は明言されてはいませんが いずれ FPU は 廃止されるようです。よって、今後は FPU や MMX に依存したアプリを組むのは避けるべきでしょう・・・

XMM レジスタが増設されたことにより、ひと命令で32ビット幅の単精度浮動小数点値を最大4組同時に演算できるようになりました。しかし、初期のSSE 命令体系ではでは物足りず、SSE2 ~ SSE4.2 へと序々に拡がってきました。

C/C++ 言語で浮動小数を扱いたい場合、一般的には float や double といった変数の型を用いる。前者が単精度浮動小数点数、後者は倍精度浮動小数。単精度は32ビット、倍精度は64ビットの幅。
細かく言えば、80ビット幅の倍精度や128ビット幅の四倍精度、八倍精度などもあります。が、これを記している時点で一般的ではないので省きます。

ゲーム等で大雑把に計算で良いならば単精度でも足りるでしょう。が、映像、画像や音などを扱うアプリではなるべく高い精度の演算結果を得たくなる。なぜなら、演算の精度が低いほど不鮮明、ノイズが混じるなど残念な処理結果に繋がるから。
結果重視ならば、単精度よりも倍精度で演算したいところです。しかし、初期のSSE では扱えませんでした。
実際に使い勝手が良くなったと感じたのは SSE2 以降。西暦2000年暮れ頃に登場した Pentium 4 で SSE2 命令が実装、つまり、利用可能になりました。
SSE2 では倍精度を2組、さらに 32ビット幅の整数4組もしくは64ビット幅の整数2組同時に扱う等の命令が追加されました。その後のSSE3 や SSE4.xx は水平加算や文字列処理に特化した補強が加わりました。

遡って、整数の加算や乗算はSSE が実装される前の MMX 命令で対応することもできました。ただし、MMX で乗算を施す場合、16ビット幅を基本となっていることもあり、SSE2 でひとつの命令で済むのに比べ、上位ビットと下位ビットを掛け合わせて足すといった具合で面倒でした。
ちなみに、SSE2 から XMM レジスタを用いて倍精度浮動小数の四則演算が可能となりました。SSE2 から SSE4.2 に至るまで、整数の除算は実装されていません。

XMM レジスタや SSE2 命令 が追加されたことにより
・自由に使えるレジスタ数が増えたという利点だけでなく、
・同じ待ち時間で2倍もしくは4倍の演算をこなせるようになった点も大きい。
これを書いている時点で、
FPU のスタックを介して倍精度の乗算や除算を行うクロック数とXMM レジスタを用いて2組の倍精度演算を行うクロック数はほぼ同じ。

そろそろ、この辺で前話と結びつけていきましょう。
32ビット版のアプリでレジスタの本数が足りず、速度低下に陥いりそうな箇所はどうしたら良いだろうか???
汎用レジスタはループ回数や数値のロード、ストアする場所、演算はXMM レジスタに任せることを意図したソースコードに書き換えるのが解消のカギとなる。

まずはお手軽なところから。SSE2 命令を積極的にコンパイラに任せてしまう方法が楽。なお、開発環境として Microsoft の Visual Studio 、C/C++ 言語の利用を想定している。

アプリをビルドする際、
/arch:SSE2 オプションを指定することで可能なかぎりSSE2 命令に置き換わる。
プロジェクトの 「プロパティページ」 ダイアログ ボックスを開く。



ダイアログボックスの左側ペイン「構成プロパティ」、「C/C++」 フォルダーをクリック。
「コード生成」プロパティページをクリック。



ダイアログボックスの右側、「拡張命令セットを有効にする」をクリックし、
ストリームSIMD 拡張機能 2 /arch:SSE2を選択する。

コンパイラに丸投げともいえるが、ビルド時に出力されたアセンブリー言語ファイルを眺めればほどよい感じで最適化されている。
ソースコードで float や double など浮動小数の演算を意図した箇所はおおむね XMM レジスタが使われるように置き換えられる・・・

ところで、
/arch:○○○ オプションを指定しなかった場合はどうなるの???
32ビット環境向で普遍的なソースコードを書きビルドした場合、汎用レジスタと FPU を介したコードが生成されてしまう。登場したての32ビット版 CPU でも動作するようなアプリが生成される。もちろん、古い世代の CPU でも実行できるという点では感心する・・・

もうひとつ。64ビット環境では /arch:SSE2 の指定は不要。少し前の部分で「64ビット版の アプリでは FPU や MMX がサポートされない」と書いた。
浮動小数を扱う演算はXMM レジスタを介して処理することがデフォルトとなっている。C/C++言語で、float や double などの浮動小数の型の変数を使った部分も自動的に置き換わる。

長くなりましたので今回はこの辺で・・・

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

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

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

[未掲載分] 「除算が遅い」の補足 (3)

今回も「除算が遅い」、「除算の高速化」などのキーワードで検索サイトからお越しいただいている方々への対応記事となります。

前話
・整数の除算ならば、「乗算とシフト」に置き換えると良いと述べた。また、過去記事除算が遅いで取り上げたように
・浮動小数点の除算ならば、「逆数の掛け算」に置き換えると良い。

前話前々話で想定していたのは、整数の除算で商を求めるケースである。高速化のために除算命令を避けたい旨で綴った。実際には、商ではなく余り ( 剰余 ) を求めたい場面もある。
除算命令を用いるならば



EAX レジスタに 商、EDX レジスタに 余りが入る。

検索サイトからお越しいただいた方の中には
除算命令を直接発行せずに余りを求めるにはどうしたら良いだろう!?!?
と疑問を抱くヒトもいるかもしれない。余りの求め方は難しい話ではないので省いてしまった。

A = B ÷ C を基に考えるとしよう。



余り = B - ( A × C ) となる。



おそらく、A = B ÷ C の演算直後であれば A と C の値はレジスタ ( 演算器 ) に残っているハズ。
商を求めるには乗算とシフト、余りを求めるには乗算と減算。おおむね、加算や減算およびシフトは 1クロック、乗算は3 ~ 5 クロック。よって、全てのクロック数を足しても除算命令を1回行うよりもはるかに速い。
しかし、レジスタに空きが無い場合、一旦、演算結果をメモリ上にストア、ロードしなおすなどロスが生じてしまう・・・


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


※ これを下書きしたのは2012年夏の終わり頃です。
ご覧いただいている時期によっては状況が異なっている可能性があります。
下書きした頃の環境は、OS - Microsoft Windows 7、開発環境 - Visual Studio 2008 , C/C++ 言語。

まず、頭に入れておきたいのはCPU に近いほどデータのアクセスが高速になる点。
データへのアクセスが速い順に挙げるとレジスタ内の数値、メモリ上の数値データ、ディスク上のデータ・・・となる。メモリを CPU に直結されたキャッシュメモリとソケットに装着するメインメモリと分けた場合、前者のアクセスは、後者よりも高速。

大雑把な言い方だが、レジスタの空き状況は実行環境ごとに異なる。64ビット版アプリは、32ビット版に比べレジスタ数に余裕がある

自動車でエンジンに該当するのは、PC の中で CPU と呼ばれている。これを書いている時点では Core i7 や Atom 、Core 2 Duo などが搭載されている。これらはIntel の i386 ( 80386 ) の流れを汲むCPU。
i386 の中で一般的な計算 ( 整数の四則演算 ) を担当するのがEAX , EBX , ECX , EDX と呼ばれる4本のレジスタ ( 演算器 ) 。ほか、ESI と EDI などのレジスタもあるが、これらはロード、ストアつまり読み出しや書き込みの場所を指す役割を担っていた。

EAX や EBX と書いたところでイメージし難いかもしれない。レジスタの幅を図で表すと以下の通り。



上の図は右側を軸とし、左方向へいくほどビット幅が広がることを意図している。

i386 の祖先は8086 や 80286 等の16ビット版のプロセッサ。それらの CPU には AX , BX , CX , DX と名前を持つ16ビット幅のレジスタが計4本備わっている。それぞれのレジスタはある程度役割が決まっていて、AX は 演算、CX は繰り返し処理の残り回数、AX と DX を組み合わせて 32ビット幅の数を扱うなどなど。

i386 はそれら16ビット版 CPUと互換性を保ちながら拡張が施された。各レジスタの数値幅は上位16ビット分拡張され、ひとつのレジスタで32ビット幅の数を扱えるようになった。
それぞれのレジスタの頭に「E」が付く。図にあるとおり、EAX レジスタの下位16ビット分が従来の AX レジスタを兼ねている。例えば「E」を付けずに「AX」と指定して書くことで16ビット幅の数値を扱うことも可能。
他方、レジスタの本数は 16ビット版 CPU と同じまま、つまり増えていない。

個人的には、32ビット版 CPU とともに扱えるレジスタ数が増えるだろうと期待していた。同じ頃の対抗 CPU を見ると、8 から 32 本のレジスタを備えた製品が登場していた。おそらく、設計・開発の段階で深い検討の末、増やさないことに決定したのであろう。

仮にレジスタが増設されたとして、消費電力が増加するだけではない。当時、マルチタスクの OS が普及することが予想されていた。ひとつの OS 上で複数のアプリを同時実行が提唱されていた。
それまではシングルタスクが主流で、ワープロや表計算ごとに別々の PC を用意していた。もちろん、一台の PC でフロッピーを入れ替えて電源を入れなおすことで別のアプリを起動することも可能であった。しかし、昨今の PC に比べ起動に時間がかかり、限られた時間の中で作業を進めるには複数台用意したほうが効率的であった・・・

人間から見て、ひとつのデスクトップでワープロや表計算やWebブラウジングを同時に実行しているように感じるヒトも多いハズ。PC の中では、ごく短い時間でそれぞれのアプリの実行を切り替えているのだ。ほかのアプリに切り替わる際、現状のレジスタ内容をどこかに保存する。実行する順番が来たアプリは、以前保存した内容を読み出し( 同じ状況に戻し )、続きを遂行する。同時に立ち上げるアプリが増えるほど、レジスタの本数に応じたメモリが必要となる。現在と比べメモリは高価であった。

簡単にいえば、、作業員を増やすほど、ロッカーや作業机その他諸々も増す必要が生じコスト増加につながる。

つづいて、昨今の64ビット対応 CPU のレジスタは以下の図のよう。



例えば、EAX レジスタの 上位 32ビット分が拡張された。64ビット幅のレジスタを RAX と呼ぶ。こちらも互換性を重視した拡張である。RAX の下位 32ビットを従来の32ビット幅レジスタの EAX として使うこともできる。また、その下 16ビット分も同様。

64ビットに拡張された際、以前 ( 16ビットから32ビットへの拡張 ) と異なる点は利用できるレジスタの本数が増えたこと。汎用レジスタは R8 から R15 の 8本 が追加された。
省電力の技術が向上したのに加え、メモリも安価になり、大容量のメモリが搭載されるようになってきた。

レジスタ数に余裕がある
ということは演算結果を一旦メモリ上に保存したり、読み込み直す可能性が減る。ロードとストアの回数が増えるほど高速化から遠ざかる。したがって、高速化したい箇所でレジスタ数が足りているか否かをチェックすると良い。

乗算や加算、減算、シフトでレジスタを使うほか、
・どこのメモリ ( 値 )を読み出すか
・演算結果をどこに格納するか
・ループカウンタ、つまり、繰り返し処理を残り何回行う?
などでもレジスタが必要・・・

i386 の流れを汲むCPU の仕様を見るかぎり、32ビット版アプリで自由に使える汎用レジスタは8本、64ビット版アプリでは16本となっている。が、そのうち2本はスタックポインタとなっていて自由に使えない。スタックポインタの値を変更することはアプリの暴走に繋がる・・・

手段としては、アプリをビルドする際にアセンブリ言語ファイルの出力を行い、
「レジスタ不足を補うための退避や復元命令が含まれていないか???」をチェックする。

※ アプリをビルドする際、アセンブリ言語ファイルを出力する方法は車輪の再発明 (7)で触れています。



ファイルの出力方法は何通りかの選択肢がある。その中で
「アセンブリコード」「コンピュータ語コード」「ソースコード」を選択すれば、C/C++ 言語のソースコードがどのようなコンピュータ語に翻訳されたか比較しやすいハズ。
出力されたアセンブリ言語ファイルをメモ帳などで開き、もとの C/C++ 言語のソースコードと一致する箇所を確認する。該当箇所近辺に

push や pop レジスタ名
mov ~ DWORD PTR [esp + △△ ] , レジスタ名
mov ~ QWORD PTR [rsp + △△ ] , レジスタ名
といったコードが含まれているかもしれない。
esp や rsp はスタックポインタ名。スタックポインタを簡単に説明すれば、手一杯なとき一時的にデータを預けるとして、預かり場所の案内係。
これらのコードはメモリ上にレジスタの値をストアもしくはロードする。コンパイラが「この部分でレジスタが足りない」と予想した結果、退避や復元のためストアやロード命令が追加される。

演算を高速化したい部分はたいてい反復処理、ループ化する。ループカウンタを扱うためのレジスタが空いてなければ

inc [esp + △△ ]
inc [rsp + △△ ]

dec [esp + △△ ]
dec [rsp + △△ ]

といった加算や減算のコードが含まれているハズ。もし、加算や減算の部分、つまりループカウンタの変数がレジスタに割り当てられれば

inc レジスタ名 , レジスタ名
dec レジスタ名 , レジスタ名

となる。ほかにも、残り回数を比較する部分で

cmp レジスタ名 , ◇◇
test レジスタ名 , レジスタ名

などの命令が含まれているハズ。
ループカウンタの変数は何度もアクセスされるためキャッシュメモリにヒットする。昨今のCPUはキャッシュメモリへのアクセスが効率化されており、それほど遅くならない・・・

話がアセンブリ言語レベルに偏ってしまいました。冒頭の通り OS を Microsoft Windows 、開発環境 を Visual Studio 、C/C++ 言語にで話しを進めます。
C/C++ 言語を基にアプリを作るとして、どの変数をどのレジスタに割り当てるかはコンパイラ次第。もっと言えばコンパイラの賢さやご機嫌に左右されてしまうのは否めません。なるべく、メモリではなくレジスタに割り当てられるようにレールを敷いてあげることが解決の糸口になるかもしれません。

高速化したい近辺のアルゴリズムを見直す、つまり手順を改良することで良い結果が得られるケースも多々あります。
検索サイトからお越しいただいている方々の中には
多重ループをシングルループに変更できないか検討すると良い
などの記述を目にしたのではないでしょうか。

しばしば教科書的な例として、画像処理で連続したメモリ空間にあるデータのうちx軸 ・y軸 の指す値を一律に処理するサンプルコードが載っています。

100 × 100 ピクセル分のマス目があり、処理したい数値が直線的に並んでいるとしましょう。x が 0 から 99 最大値 - 1 まで1つずつ増えてゆき、x が 100 に到達したら x を 0 に戻し y を 1 増やします。y が 100 になると処理が終了・・・といった具合。
x や y の表現よりもアナログ時計の短針、長針を思い浮かべると簡単かもしれません。長針が 0 から 59 で一周すると短針が1 増えます。現在の値は「短針の値 × 60 + 長針 の値」とひとつに纏めることができます。

一般的には変数の数が増えるほど、多くのレジスタ数が必要。この観点から、
・2つの変数を用いるよりもひとつの変数で済ませる
・二次元配列を一次元にまとめる
などのカスタマイズは有意義である。

n [x][y] = ~~~~~~ ;

といったコードを
n[y * 100 + x] = ~~~~~~ ;

に書き直すといった感じだろうか。
もっとも、コンパイラはバージョンアップ毎に賢くなっています。上記のように単純なコードであれば手動で書き直すのは不要かもしれません。最近のコンパイラを使っていれば同等の最適化を代行してくれる可能性が高いハズ。コンパイラに任せてしまうほうが楽。
メンテナンス面を考慮しても、後日でも読み易いコードを組むことは重要です。

仮に、コンパイラが適切なコードを生成しないなどの理由でカスタマイズするならば、「なぜソーソコードをこのように変更したのか」等のコメントを残すよう心がけたいものである・・・

それよりも、実行環境が64ビット版の OS ならば、アプリも64ビット版としてビルドしなおす方が効果的かも。レジスタ不足やその対処コードが追加されてしまうのは32ビット版のアプリとしてビルドした場合に顕著。



同じソースファイルからビルドしたもので比べた場合、64ビット版のアプリの方が効率的なコードが生成されるのは明らか。



64ビット版向けへの切り替え方が判らないヒトは車輪の再発明 (7)の後半をご参照あれ・・・

とはいえ、外部のライブラリ、プラグインモジュールが64ビットに対応していない、32ビット環境に縛られてしまう状況もあることでしょう。はたまた、CPU は64ビット版に対応しているにもかかわらず 32ビット版の OS を購入してしまい、64ビット版のアプリを実行できないヒトもいることでしょう。

Microsoft の Visual Studio を利用しているならば、32ビット版アプリをビルドする際、
/Oy オプションを指定することにより、フレームポインタの作成が省略されます。副作用として自由に使える汎用レジスタが1本増えます。



さらに、
/Ox (最大限の最適化) や /O1 と /O2 (実行速度) のオプションを指定した場合、/Oy オプション を指定したのと同じ効果が得られる。」とMSDN に載っているハズ。

アプリを高速化したいと考えるなら、「最大限の最適化」や「実行速度」を指定するのは自然なことである。

この辺の最適化オプションには苦い思い出がある。いくつか前のバージョンのコンパイラで「最大限の最適化」を施した場合、不正なコードが生成されるケースがあった。同じブロックで特定の条件が揃うとアプリが暴走してしまった。最適化条件を変え、「フレームポインタの作成が省略」をオフにしたり、ほかのコンパイラでビルドした場合には正常に実行できた。
出力されたアセンブリ言語ファイルを開き、「ソースコード」と翻訳された「コンピュータ語のコード」を比較することで、不正なコードが生成されていることが発見できた。

不正なコードが生成されるのは高速化とは無縁の部分だった。よって、その部分だけ「フレームポインタの作成が省略」をオフにすることでトラブルから逃れることができた。

フレームポインタの在無により問題が生じてしまう部分を
#pragma optimize( "y", off )


#pragma optimize( "y", on )

で囲うことでトラブルから脱出するに至った。この囲われた部分は最適化されなくなる。

あくまでも、Microsoft の Visual Studio 固有の機能。#pragma ~~ ディレクティブは環境に依存する。#pragma ~~ を残しておくと Microsoft の Visual Studio 以外では翻訳する際エラーとなってしまう可能性が高い。
また、同系列のコンパイラでもバージョンアップとともに使えなくなることもある。#pragma ディレクティブを使うのであれば、前後を #if と #endif ディレクティブで囲い、特定の環境では無効になるようにガードしておくと安全である・・・

32ビット版アプリを前提とした、レジスタが足りない場合のお話は続きがあります。

長くなりましたので今回はこの辺で・・・

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

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

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

[未掲載分] 「除算が遅い」の補足 (2)

今回も「除算が遅い」、「除算の高速化」などのキーワードで検索サイトからお越しいただいている方々への対応記事となります。

前話で、
・除算そのもの以外にも速度低下の要因が含まれていないか?
・除算を行う近辺の贅肉をそぎ落とせないだろうか
といった点を触れました。
除数が定数ならば、さらに高速化を狙えます。前話が長くなったため後回とした分を・・・


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


※ これを下書きしたのは2012年夏の終わり頃です。
ご覧いただいている時期によっては状況が異なっている可能性があります。
下書きした頃の環境は、OS - Microsoft Windows 7、開発環境 - Visual Studio 2008 , C/C++ 言語。

前回の続きに入る前に、

分数でいうところの分子は numerator もしくは fraction 、分母は denominator の 単語が該当。よって、x = n ÷ d や x = n / d と記したいところです。ここでは話を簡単に進めるため A = B ÷ C



と表現します。それぞれ、

A 演算結果、商。
B 被除数、割られるほうの数。
C 除数、割るほうの数。

さて、過去記事除算が遅いにおいて、浮動小数点演算ならば
・分母が定数ならば「割り算」を「逆数の掛け算」に置き換えると良い
と述べた。今回は想定しているのは整数の除算。

整数の除算において除数が固定できる場合、除算命令を使わずに済ますことが可能。結論を急いでしまえば、
・定数の乗算 ( 掛け算 ) と シフトに置き換える
ことができる。厳密にいえば、符号なし整数と符号付き整数で異なるのだが、話を簡単にするため符号なし整数を基に話を進めよう。

A = B ÷ 10 を求めたいとする ( ただし、A , B , C それぞれが32ビット幅に収まる数値 )。
16進数 0xcccccccd を掛けて、35ビット分右方向にシフトした値と一致する。

「シフト」や「ビット」や「バイト」について苦手なヒトは過去記事遅くない除算をご参照あれ。



右方向にシフトとは下方向にシフトすること。1ビット分右シフトすることは「÷ 2」と同じ、逆に1ビット分左シフトすることは「×2」と同等。



少し機械寄りな話、前回記したことと重複する。
32ビット版のOS上でも64ビット版のOS上でも32ビット版アプリを動かしている場合、ひとつのレジスタ ( 演算器 ) の幅は32ビット。
B と 0xcccccccd を掛けた値は32ビット幅に収まりきらない可能性も生じ、正しい結果を得るには64ビット分の幅が必要。
通常、乗算命令の結果は上位32ビット分が EDX レジスタ、下位32ビット分は EAX レジスタに格納される。
ということで、乗算命令の結果のうちEDX レジスタを3ビット分右にシフトした値と「B ÷ 10」が一致する。

64ビット版のOS上で64ビット版アプリを動かしている場合、ひとつのレジスタ ( 演算器 ) の幅は64ビット。
「B × 0xcccccccd」の結果もひとつのレジスタに収まり、32ビット以上のシフト操作も容易・・・

参考までに
「÷ 100」は 0x51eb851f で掛けて、37ビット右シフト、
「÷ 1000」は 0x10624dd3 で掛けて、38ビット右シフト、
「÷10000」は 0xd1b71759 で掛けて、45ビット右シフト・・・

ほかにも、○○で掛けて△△ビットシフトするの組み合わせは多数。

さすがに覚えきれないし、表計算では縦方向に制限がある。



印刷して持ち歩くのも大変なので、アプリを自作した・・・



例えば、除数を「44100」と入力すれば、0xbe37c63bで掛けて、47ビット右シフトといった具合・・・



さてさて、「除算」を「乗算とシフト」に切り替えることでどれ位の効果を期待できるのだろう。
これを書いている時点で出回っている CPU の傾向を載せる。それぞれ、CPU 名、除算に要するクロック数 ( 待ち時間 )、乗算に要するクロック数、シフトに要するクロック数は概ね以下の通り。

CPU除算乗算シフト
Core i72542
Core 2 duo4051
Pentium480111

大雑把に言えば、待ち時間が60分から10分前後へと縮まるイメージ。
細かく言えば、このクロック数は前後のメモリアクセス等を無視している。前後のメモリアクセス ( ロード / ストア) や他の命令との絡みで状況も変わる・・・

・・・とここまでの流れでは
除算を高速に処理したい部分を「乗算とシフト」に置き換えることを推奨していると思われるかも。実際のところ、
コンパイラの賢さに依存
する。例えば、C/C++ 言語 で「A = B ÷ C」のままコードを書いたとしよう。

64ビット向けアプリにビルドした場合、ほどよく「乗算とシフト」に変換された実行コードが生成される。しかし、同じソースコードを32ビット向けアプリとしてビルドした場合、除算命令や __aulldiv を呼び出すようなコードが生成されてしまう。

個人的な感触では除数が一定の数値までは「乗算とシフト」が生成される。もしかしたら、「ここは速度重視ではないので、除算命令を生成しよう」とコンパイラが判断した可能性も否定できない。詳しくは両方をビルド後にアセンブリ言語の出力ファイルを比較すれば良い・・・

高速さを求める部分で「A = B ÷ C」ではなく、「○○で掛けて△△ビットシフトする」のコードを意図的に書くのは「除算命令を生成させない」という意味で有効。以下の点も頭に入れておきたい。

・コンパイラのバージョンアップにともない改善されてゆく。
・将来的には、除算の待ち時間がさらに短くなるよう、ハードウェア面で改良されてゆく。
・保守する際に複雑。
とくに多人数で作成に携っている場合、トリッキーなコードはトラブルのもと。オリジナルコードとなぜこのコードに置き換えたのか等のコメントを忘れないようにしたいものだ・・・

ではでは、除数 ( A = B ÷ C の C の値) がランダムな場合でも
「乗算とシフト」のテーブルを埋め込んでおくことで高速化できるのでは?
ちょっとビミョー。除数の範囲が狭いならば高速化を期待できそう。テーブルがキャッシュメモリに収まるか否かで速度が格段に違ってくる。

乗数とシフトカウントは4バイト + 1 バイトでパックできる。一般的な PC の構造を考慮した場合、4バイトの倍数にデータが整列配置されていないとメモリから値を読み取る時に遅延が生じる ( 可能性がある ) 。
とすれば、「乗数とシフト」ひとつの組み合わせで8バイト。ちなみに、これを書いている時点で、一般的な PC の L1キャッシュは 32768バイト。ということは、32768÷8で
4096通りまでは L1 キャッシュに収まるので高速化を狙える???

現行の一般的な OS は複数のタスクを切り替えて実行している。人間から見れば、ワープロや表計算や Web ブラウザーを複数同時に実行しているように感じるのだが、ごく短い時間毎に切り替えている。
ほかのアプリに切り替わる際、たいていL1 キャッシュの内容が変更されてしまう。おそらく、再び実行の順番が巡ってきた際、L1キャッシュに残っていないなら L2 キャッシュなどから読み直す。さらに L2キャッシュにもデータがなければ・・・

Core i7 以降はメモリアクセスが改善されています。メモリからのロードを含めても、除算命令を遂行するより待ち時間を短縮できるかもしれません・・・

続きは後日。

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

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

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

[未掲載分] 「除算が遅い」の補足 (1)

当ブログで過去に、



除算が遅い
遅くない除算
三角関数はもっと遅い
等の記事を掲載しました。その続編も準備していたのですが、諸事情により見送りました。
「除算が遅い」、「除算の高速化」などのキーワードで検索サイトからお越しいただいている方々への対応・・・


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

アプリを作ってみたところ、
「除算が遅いので何とかしたい!」。その心意気、何かに挑もうとする姿は応援したくなる。
しかし、車輪の再発明 (1)から車輪の再発明 (11)辺りで述べた通り、何もかも高速化、最適化、チューニング、すれば良いというワケではない。最適化作業に掛かる労力、時間、費用が見合うか否かの見極めも肝心。
現状で提供されているハードウェアで時間が掛かるとしても、数年後に技術改良が施され高速に処理できるようになる可能性もある。
数年前まで一定の時間内でより多くの演算をこなせるかの方向で進化してきた。手元にある Intel の CPU を搭載した PC で試してみたところ、Pentium 4 から Core 2 Duo への進化で除算に要する時間 ( 待ち時間 )は約半分、Core 2 Duo から Core i7 では 33%減と改良されている。
ここ数年は、高速性の追求はひと段落し、省電力、小スペースの方向へ進化を続けている・・・

※ これを下書きしたのは2012年夏の終わり頃です。
ご覧いただいている時期によっては状況が異なっている可能性があります。
下書きした頃の環境は、OS - Microsoft Windows 7、開発環境 - Visual Studio 2008 , C/C++ 言語。

過去記事除算が遅いにおいて、浮動小数点演算、整数演算ともに
・乗算は遅くなりにくい。
・除算は加算、減算、乗算に比べ数倍、数十倍の時間を要する。

このことから、
・分母が定数ならば「割り算」を「逆数の掛け算」に置き換えると良いと綴った。
実際のところ、近代的な開発環境を利用しているのであれば、コンパイラが自動的にこの辺の置き換えを行ってくれる・・・

ところで、検索サイトよりお越しの方々の探している情報は
「アプリの実行速度を上げたい」
といったところだろうか?
ついつい、「高速化のために何かを加える」と考えガチである。その「何か」に当たるエッセンスや裏技的なモノを探したくなる気持ちも判らなくはない。その方向性ならば、アプリの改修に四苦八苦するのではなく、資金を惜しまず、最新鋭かつグレードの高い機種を導入すれば悩みは解消するだろう。

それよりも、「阻害要因の除去」に視点を切り替えてみよう。悩みを解消するカギが見つかるかもしれない。
除算命令の遂行そのものが遅い。それ以外にもアプリの実行速度を低下させる要因があるのではないかを探り、「速度低下のもとを削る」と考えれば道が開けるかも・・・

OS やアプリが32ビットなのか64ビットなのか、さらに浮動小数点演算なのか整数演算なのかでそれぞれ話が違なる。今回は32ビット環境、整数演算を前提に話を進める。

除算の話に入る前に、「32ビットって何」というヒト向けに補足。「ビット」「バイト」に関しては過去記事 遅くない除算 で触れていますので省きます。

PC を購入する際、カタログのスペック ( 仕様 ) 覧を眺めていると、CPU の種類や搭載メモリ容量などが載っています。PC でいう「CPU」とは自動車でいえばエンジンにあたる部分。CPU の項目にはCore i7 、Pentium や ATOM , 少し前ならば Core 2 duo などと表記されていることでしょう。これらの CPU は Intel 80386 の流れを受け継ぐプロセッサー。



Intel 80386 ( 以降「i386」と記す ) は 西暦1985年頃登場、i386 の 2世代後がPentium、4~5年後に後継の Pentium II と廉価版の Celeron が登場しました。現在でも Pentium や廉価版 ( 機能削減版 ) の Celeron といった 名称が受け継がれています。

CPU の進化の流れに関しては車輪の再発明 (8)でも触れましたので興味のある方はご参照あれ。

i386 の登場により、レジスタ( 演算器 ) が32ビットに拡張された。32ビット幅の数値を扱いやすくなった。
「1ビット」が「0か1の2通り」、つまり「32ビット幅の数値」とは「2×2×2×...2」と32回掛け算した値。符号なし整数ならば 0 から 4294967295、符号付き整数 -2147483648から+2147483647までを簡単に扱えるようになった。
大雑把に書くとすれば、「プラスマイナス21億までの計算がしやすくなった。」と言ったところ。

それまでのPC、いわゆる「16ビットCPU」 ではプログラミングの際、16ビット幅の数値を標準としていた。
16ビット幅の数値というのは、符号付き整数 -32768 から + 32767、符号なし 0から65535を扱える。実際の給与計算などを想定した場合、この数値幅では足りない。
では、「16ビットCPUが搭載されていた頃の PC で16ビット幅を超える数値を扱えない???」という誤解も生じやすい。
32ビット幅の数値であれば16ビットずつ上位、下位に分けて扱うことは可能であった。
ちなみに、「i386 が64ビット幅の数値を扱えるのか否か?」も同様で、ひとつのレジスタでは扱えないが上位、下位に分けて扱うことは可能である。

上位、下位に分けるという表現が難しいかもしれない。例えば、スイカ4玉を運ぶように頼まれたとしよう。旧来は2玉ずつ2回に分けて運んでいた。16ビットから32ビットへ拡張されたことで4玉を一度に運べるようになった。
同様に、32ビットから64ビットへ拡張され「スイカ8玉を一度に運べるようになった」。・・・「4トントラックで1往復で運べる」か、「2トントラックで2往復するか」の違いと表現したほうが判り易いかも・・・

演算結果は正しいのか???

さてさて、スイカを運ぶだけの話ならばここで終わり。わざわざ「32ビット幅」や「上位」「下位」「有効桁」など遠回りに思えるかもしれない。32ビット環境では「桁あふれ」への対処部分が速度を落とす ( 遅くなる ) 原因となりうる。

例えば 0から99の2桁(100通り)の数。2つの値で掛け算した場合、最も小さいのは0、最大は 99×99 = 9801。
もし、下2桁しか表現できなければ、上2桁は無視される(無効になる)。
66 × 77 の答えは5082。しかし、下2桁しか扱えない ( 上2桁が示されない ) ならば「82」となり不正解。

人間なら多少の融通が効く。現在西暦2012年だとして、日常的な会話の中で「95年」と出てくればおそらく「西暦1995年のこと」と推測できるし、「10年の春」と聞けば「平成10年の春」もしくは「西暦2010年の春」のどちらかを会話の前後から判断できる。しかし、機械はこの辺が苦手。

過去記事、2012年12月17日分で、アナログ的な体重計、1周100kg を例に挙げた。



・体重 130kg の立派な体格のスポーツ選手
・体重 30kg の小学生



どちらが体重計に乗っても針は 30kg をさす。
人間の目で考えれば、子供なのか立派な体格のヒトが乗っているのかは区別が難しくない。ところが、機械にとっては両方の区別が難しい・・・

典型的な式を使って考えてみよう。


Aが「66」Bが「77」、Cが「44」と仮定。A×Bの段階で「5082」、それを「44」で割れば「115と余り22」が正しい。もし上位桁を正しく処理できない場合、A×Bの段階で「82」、「82÷44」の答え「1と余り38」となってしまう。
桁あふれへの対処を怠ると正しい結果を得られないことがある・・・

演算結果の上位はEDXレジスタ、下位はEAXレジスタ。
(1-1) 下位32ビットを指すEAXレジスタに66、上位32ビットを示すEDXをゼロにする。
(1-2) EBXレジスタに「77」を入れ、EAXレジスタと乗算。
(1-3) ECXレジスタに「44」を入れ、EDX:EAX レジスタ内の値(被除数)をECXレジスタで割り、結果EDX:EAX レジスタにストアする。
もし、アセンブリ言語を理解できるレベルのヒトであれば、の3ステップで通じるハズ。MOV 、MUL 、DIV の3種類 (ゼロクリアに XOR 命令 を使うとしても4種類の命令) を用いて「A×B÷C」となる。

おそらく、検索サイトからお越しの方々はアセンブリ言語を理解できるレベルまで到達していないことだろう。さらに、Visual Studio で 64ビット用のアプリ作成する際にはインラインアセンブラが利用できない。
少し緩めて、C/C++ 言語でも通じるように進めよう。

「A×B」の結果が32ビット幅を超えてしまう可能性がある。さらに、「÷C」の結果も32ビット幅に収まらないかもしれない。
64ビット版のWindows が登場する前、つまり32ビット版のOSで32ビット版のアプリを作成することが前提だった時代でもこの辺は考慮されていた。
教科書的な書き方をすれば、C/C++ 言語で32ビット版のアプリで64ビット整数型 (64ビット幅の数値) を扱うには long long や unsigned long long を用いる。

過去記事 車輪の再発明 (5)車輪の再発明 (9)でも触れた通り、Microsoft の Visual Studio に限定すれば LONGLONG や ULONGLONG、LONG64やULONG64などの変数型を用いることも可能である。さらに、LARGE_INTEGER や ULARGE_INTEGER といった構造体も容易されている。



リトルエンディアンを想定して下位、上位といった並びになっている。
LARGE_INTEGER の 使い方も難しいところはない。32ビット幅に収まる数値、66を代入するしたいならば
LARGE_INTEGER li;
li.HighPart = 0;
li.LowPart = 66;

LARGE_INTEGER li;
li.QuadPart = 66;

と書くことができる。

さらに 77 で掛けて、44で割りたいならば
ULONG32 uB = 77 , uC = 44;
li.QuadPart = li.QuadPart * (ULONG32) uB;
li.QuadPart = li.QuadPart / (ULONG32) uC;

と書ける。
実際のところ、近代的な開発環境を用いる限り、定数での除算はコンパイラが適切な乗算に置き換えてくれる。



「将来の移植を考え、LARGE_INTEGER 構造体を用いるように」と提唱されていた覚えがある。が、いつ頃からだったのか定かではない。Visual studio 6.0 付属のヘルプディスクを漁ってみたところ、Windows NT 3.1 やWindows 95 以降となっていた・・・

そろそろ、本題。削減できる可能性が高い部分を探るため、32ビット向けに生成されたアセンブリ言語コードと64ビット向けに生成されたコードを比べる。詳しくないヒトのために補足すると、「機械が直接理解できる命令」を診ることで遅くなる原因を探る。

アプリをビルドする際、アセンブリ言語ファイルを出力する方法は車輪の再発明 (7)で記したので省く。

「A」「B」「C」はそれぞれ32ビット幅の範囲の数値、ただしゼロではない。「C」の値は毎回変わるとする。
「A×B÷C」の「÷C」の部分がどのように翻訳、機械が直接理解できる命令に変換されるのかを確認してみよう。

まず、32ビット環境向けに生成されたコード。



次に64ビット環境向けに生成されたコード。



64ビット向けのコードは 数値を代入する mov と divでシンプル。
xor edx,edx は mov edx,0と同等。作業台の上に移動するのが mov 、div で割り算を行っている。

それに比べ、32ビット版のコードはシンプルとは言えない。
数値を代入する mov 命令ではなく、push ~~、割り算を意味する div 命令の部分は call ~~となっている。
push の部分は作業台の上がいっぱいなので、棚や倉庫など別の場所に置き換えるようなもの。call 命令はサブルーティンを呼び出す。
表現が適切かビミョウだが、64ビット向けのコードは自前で処理、32ビット向けのコードは外注するくらい意味が異なる。

ちなみに、MSDN などで __aulldiv を検索しても欲しい情報に辿り着けない可能性が高い。
__aulldiv の部分がどうなっているのか知りたいならば、Visual Studio をインストールしたフォルダー内にソースファイルが含まれている。



拡張子「.asm」でファイルを検索を行い、該当したファイルの中から「lldiv.asm」や「ulldiv.asm」を開いてみると参考になる。
これらのファイルはアセンブリ言語で書かれている。アセンブリ言語を読み解くのは難しいかもしれないが、コメント部分を読めば概要は把握できるハズ。

64ビット環境であれば32ビット幅の数値の除算が div 命令ひとつで済むのに比べ、条件比較や移動、多くの分岐処理が行われる。
「lldiv.asm」や「ulldiv.asm」を眺めるかぎり工程数が多く感じてしまうが、32ビット環境において、より正しい結果を得るために最低限必要な工程。さらに、サブルーティンの呼び出し、戻りも実行コストが増える・・・

__aulldiv を呼び出さない、直接 div 命令に翻訳されるようにコードを修正する
ことで手間を軽減できる可能性が高い。
判り辛い表現かもしれない。例えば、書類のコピーが必要だとしよう。職場、作業場で身近にコピー機が備わっていない。その場合、「ちょっとコンビニまで出かけて・・・」となる。一日に一・二回なら休憩も兼ねてコンビニまで出かけるのも良いだろう。しかし、何回も繰り返すならば、身近にコピー機が備わっていることに比べ手間が増える。その手間を削る策を練るということだ。

全ての除算が __aulldiv を呼び出すように翻訳されるのではない。被除数、つまり「A×B」の結果が32ビット幅に収まりきらない可能性があるとコンパイラが判断したことにより __aulldiv を呼び出すコードに翻訳される。
逆にいえば、演算結果が32ビット幅に収まることが明確ならば除算部分で __aulldiv を呼び出すコードの生成を避けられそうだ。

例えば、「A」「B」の数値がともに16ビット幅、と決めうちできれば「A×B」の結果が32ビット幅に収まる。
そのような場合 64ビット幅の LARGE_INTEGER 構造体 を用いる必要は無い。「A」「B」は short や unsigned short 型 で扱い、「A×B」の結果は int や unsigned int 型 へ納めるば良い。

ほかにも24ビット幅と8ビット幅の数値の掛け算などでも良いのだが24ビット幅の数を扱うには少々複雑なので今回は略。
要は、被乗数と乗数 ( 掛けられる方の数と掛けるほうの数 ) のとりうる値のビット幅を足して32を超えなければ良いのだ。

人間が見て演算結果が32ビット幅に収まる場合でも、コンパイラのご機嫌や賢さに依っては__aulldiv を呼び出すコードに翻訳されることもある。
それだけではないが、人間が「ここは○○」が明確と判断できる部分でもコンパイラには判断できないケースも生じる。
オプティマイザによる最適化が足りない。まだまだ最適化の余地が残ってしまう。オプティマイザとは翻訳の後、最適化を行う担当者といったところ。

開発環境が Microsoft の Visual Studio で C/C++ 言語を用いている場合に限定して言えば、__assume キーワードを挿入することで改善される可能性が高い。詳しくは__assumeをご参照。
MSDN では switch ステートメントの例を取り上げているハズ。無駄な条件比較、分岐が削減される。
ほかにも、ポインタのNULL チェックを削減したい、演算前の条件比較を削減したい、for や whileなどループ部のカウンター初期値のチェックを省きたいといった部分で __assume キーワードを挿入することにより、より最適化されたコードが生成される。

__assume キーワードはMicrosoft 以外の コンパイラでは利用できない。ほかの開発環境での利用を考え
#if defined(_MSC_VER) && (_MSC_VER >= 1000)
__assume( 条件式 )
#endif // defined(_MSC_VER) && (_MSC_VER >= 1000)

のように使うのが無難である。

まれに、for 文の前に __assume キーワードを追加しても無駄、効果が無いといった記述を見かける。筆者が眺める限り、使い方が間違っているだけに思える。__assume キーワードの位置が合っていないと無駄になるの間違いでは・・・

#if defined(_MSC_VER) && (_MSC_VER >= 1000)
__assume(i>0);
#endif // defined(_MSC_VER) && (_MSC_VER >= 1000)

for(ULONG i = XXX ; i ; i --){~~~

こんな書き方だと __assume キーワード が無駄になる。理由はカウンターとなる変数の定義位置。
ちょっと手直しして、カウンターとなる変数をfor の外で定義します。
ULONG i = XXX;

#if defined(_MSC_VER) && (_MSC_VER >= 1000)
__assume(i>0);
#endif // defined(_MSC_VER) && (_MSC_VER >= 1000)

for(; i ; i --){~~~

筆者の環境ではこのように修正することで、ループ部のカウンター初期値のチェックを削減できました。
適切な使い方をするかぎり、Visual C++ 6.0でも __assume は無駄ではありません。

証として、Visual C++ 6.0 の生成したアセンブリ言語コードを載せます。
まずは、__assume キーワード の位置が誤っていると思われる方。



次に、__assume キーワード の作用により最適化が向上したコード。



パッと見ただけでも短くなっているのが判るのではないでしょうか。
興味があるヒト向けに補足しますと、「$L128968:」がループ部分の先頭、前者の00031でゼロか否かの比較分岐があります。一方、後者は「$L128968:」直前のゼロ比較が取り除かれました・・・

ここまで飲み込めれば、他にも削減できそうな部分に気が付くことでしょう。先の「A×B÷C」で考えれば、いずれの数値もゼロでないことが明確ならば 先ほどの __assume キーワード を用いることで無駄なコードを削減できるハズ。

今回は「C」の値が変わる前提です。よって、変数の値がゼロになることも想定されます。どれかの変数がゼロならば演算する必要はありません。
厳密には、「C」がゼロならゼロ除算エラーの対処コードを書きます。しかし、今回は話を簡略化するため「C」の値がゼロの場合でも演算結果ゼロを返すとして話を進めます。

演算の前に各変数のゼロチェックを行うこと。いずれかがゼロなら乗算と除算をスキップ させます。

教科書的な書き方をすれば、条件判断の部分は
if (A== 0 || B == 0 || C == 0) ~~~
もしくは
if (A!= 0 && B != 0 && C != 0) ~~~
といった感じでしょうか。それぞれ
if (!A || !B || !C) ~~~

if (A && B && C) ~~~

と書いても同じ意味です。
ビルドしてこの部分のアセンブリ言語コードを確認すれば、ゼロ比較と分岐が3組並ぶハズ。

書籍やWebサイトに「高速化のために条件判断や分岐を減らそう」と載っていると思います。
この例も工夫することで分岐を2回分減らせるかもしれません。

例えば、論理演算のうち 論理積 ( AND 演算 ) が役に立ちそうです。
この辺の話題が苦手なヒトのために書けば、2つの変数の値のうちどちらかがゼロならば演算結果はゼロ。

if (A && B && C)・・・


if (((A & B) & C) != 0)・・・
などに変更します。

「A」と「B」のAND値と「C」のAND値を取得しゼロと比較。ゼロでないならば~~~を実行する・・・となります。
どれかひとつでも変数の値がゼロであればAND 値がゼロになります。これでゼロ比較と分岐は1回で済むハズ。

ここまでは除数が固定されない場合のお話。除数が定数の場合はもっと絞り込めます。

続きはまた後日・・・

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

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

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

[検索語彙への応答] 信長の野望 嵐世記 (7)

今回も「嵐世記 攻略」などで検索サイトから訪れるヒトへの応答。



なかには、攻略するツール等を探して当ブログを訪れるヒトもいることだろう・・・

パワーアップキットに付属する武将データの編集機能を利用すれば、だいたいプレイヤーの想いに近い状況を作り出せる。
一方、武将の健康状態、実親や義理親といった出自、敵国に潜伏させる忍者の能力や派遣期間、城郭の最大防御度合、国の石高 (その地域の米の収穫高) などは変更できない。
さらに、ミッションチャレンジでは編集機能が使えないなどの制限がある・・・

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

「嵐世記 攻略」という単語で検索サイトから訪れる方々の全てに対してではないが、中には
攻略用アプリそのものをダウンロードできる場所やソースコードを探していたヒトもいるようだ。
それらに直接応じることは、わが国の法律に抵触する恐れもある。

嵐世記が発売されてから10年以上経つ。この世代のゲーム等のデータを書き換えるには
ディスク上にセーブされたデータを変更する
現在プレイ中のデータ、つまり PC のメモリ上の数値を直接書き換える
などが考えられる。

ディスク上に保存されたデータを改ざんすることは、一見簡単そうに思える。バイナリエディタで開いてみれば16進数が並んでいることを確認できる。



しかし、案外うまく行かない。データを生のまま保存するアプリも多い。が、作り込まれたアプリであれば保存する際に暗号化する。つまり、生データのまま保存しない。
例えば、嵐世記の前にリリースされた烈風伝のデータは、特定の数値をビット演算することで生データに変換できた。しかし、ほかも含め、生データに戻すカギとなる数値が毎回同じとも限らない。毎回カギとなる数値を算出するとなると手間がかかる・・・

となると、プレイ中にメモリ上の数値を書き換えるのが良いだろうか?
古くからプロセスメモリエディタやデバッガと呼ばれるフリーウェアが存在しており、起動中アプリのデータ領域を確認したり書き換えることが出来た。これらツールは本来、アプリの開発中にバグを探す、想定外の動作をする要因を突き止めるのに役立てるものだ。が、ゲームのパラメータ書き換えにもしばしば使われる。

予め述べておくが、無闇に書き換えることはアプリの暴走を引き起こす可能性がある。
そもそも、メモリ上の数値を書き換える行為はセキュリティ上好ましくない。悪意のあるアプリなどはこの手法で外部からバッファオーバーランを仕掛けてくる場合もある。

Windows Vista 以降、Windows 7 、Windows 8 などでは外部からメモリ内容を簡単に書き換えられないような仕組みが加わった。ASLR と呼ばれるアドレス空間配置のランダム化やカナリアやクッキーを用いてのスタック領域保護などなど。
スタック領域保護はアプリを作る側の課題であり、一般的にアプリを使うヒトであれば気にする必要はない。
※ Visual Studio 2005 以降は「クッキーによるスタック領域保護」機能を持ったアプリを作成可能。

メモリエディタの紹介や使い方について扱っているサイトは多々あります。それらをお望みの方は他所様のサイトをご覧いただきたい。
ここでは、陥りがちな点を述べます。不慣れな段階においては、
確認する際、データの並び順を勘違いし易い
過去記事、2012年 8月17日分でビッグエンディアンとリトルエンディアンについて触れてあります。

例えば、999という数値を探したいとしましょう。
私たちが日常的に使う10進数で考えると999です。一方、PC の中では 2進数で扱われ、16進数で表現されます。
10進数は一桁が0から9となり、9の次は10。それに対し、16進数は9の次はA,その次はB・・・と続き15を表すFの次が一桁繰り上がって10といった具合・・・

999を16進数で表すとどうなるか確認してみましょう。特別なツールは不要です。Windows に付属している電卓で変換できます。
電卓を起動し、「999」を入力。



画像はWindows 7 で電卓を起動した例、メニューバーの表示(V) をクリックし、関数電卓に切り替えてあります。
「16進」のラジオボタンをクリックすれば、16進数に変換されます。



電卓には 3E7 と表示されているはずです。
16進数で32ビット幅の数値を現すと8桁、64ビット幅の数値は16桁になります。32ビット幅ならば、000003E7 と表すべきですが、冒頭のゼロが5桁続く部分は省略されています。

16進数のE は14を意味します。
10 進数で999 を (9 * 10 * 10) + (9 * 10) + 9 と書けます。意図的に各桁を括弧で括ってありますが、本来括弧は不要。
同じように「3E7」を紐解くなら
(3 * 16 * 16) + (14 * 16) + 7 となり、
768 + 224 + 7 = 999

さてさて、バイナリディタやメモリエディタ等で 999 という数値を探したい場合、検索語として 3E7 や 03 E7 と入力してしまいがち。そして、見つからないことも・・・

過去記事、2012年 8月17日分で述べたように、データの並び順が異なっている場合があります。
00 00 03 E7 ではなく、
E7 03 00 00 の順や
03 E7 00 00 の順で数値が格納されているかもしれません・・・

さてさて、ミッションチャレンジモードの中に
・織田信長公で鉄砲を三千挺を集める
・大友義鎮公で大砲を15門集める (大友義鎮は大友宗麟公の若い頃の名前)
などのシナリオが含まれています。

通常のゲームであればパワーアップキット付属の編集機能から、大名家の財政状況を変更できる。
嵐世記のミッションチャレンジモードでは編集機能が使えない。ミッションクリアしないかぎり次の章へ進めない。
「嵐世記 攻略」というキーワードで検索サイトから訪れた方々は、この辺を解消したくてアレコレ検索していたのではないだろうか。

ゲーム画面左上に軍団長の画像と、金銭、兵糧、兵士、軍馬、鉄砲、大砲の各数値が表示されている。
それに加え、メモリエディタの使い方や16進数の並び順について判ってしまえば解決のヒントが見えてくるハズ。

かつて嵐世記の武将データの構造が詳しく掲載されているサイトがあった。現在閲覧できるか否か不明。
それらの情報を基に、標準の編集機能では不可能な部分を変更できる。病気や怪我から復帰したり、誕生年や親など出自の変更も可能。
ゲーム中、義理親が設定されている武将は親が仕官している大名家に仕官する仕組みとなっている。義理親や誕生年、登場年を変更できることを応用し、特定の武将を自動的に仕官させるように変更するのも面白だろう。
例えば、豊臣秀吉公子飼いの福島正則、加藤清正、加藤嘉明といった猛将が有名である。もし、豊臣秀吉公がプレイヤー大名家の一員であれば、子飼いの猛将たちもプレイヤー大名家の一員に加えたいところだ。
初期設定のままであれば、これら猛将は浪人として登場する。浪人を召抱えるには滞在地で登用を行うことになるが、大名家の知名度が低い、運が悪いなど条件によっては他大名家に仕官するかもしれない・・・

実際のところ、16進数と睨めっこしながら多数の武将データに変更を加えるのは手間がかかります。その辺がデータ修正用アプリを作るきっかけでした・・・



ところで、この種の話題は
メモリを直接書き換えるツールを作るにはどうしたら良い?
に行き着きがちです。
外部からのメモリ書き換えはセキュリティ上、避けたいところです。
これより後に述べるのは、あくまでも、アプリ開発時にメモリ内容を確認したかった際の例です。アプリが順調に動いているか確認するためメモリに書き出す内容を別のアプリから監視する必要がありました。
根本的に、Visual Studio 等の統合開発環境にデバッガが付属しています。昨今のデバッガは強力になりスレッドや外部モジュールまで深追いでき、深い問題点の検出も可能です。しかし、数世代前の統合開発環境に付属していたデバッガでは深追いできなかった事があり、検証ツールを自作したことがあります。

OS を Windows に限定して言えば、デバッグの補助機能に関する関数 ( 機能 ) が備わっています。それら関数を利用することで他アプリの利用しているメモリ空間を確認できます。



その頃の手順は以下の通り。
・プロセスと、プロセスが使っているヒープ、モジュール、スレッドのスナップショットを作成
・得られたハンドルから指定されたプロセスのメモリ領域からデータを読み取る
・スナップショットで得られたハンドルを返却するため CloseHandle 関数を呼ぶ

細かく知りたいヒトは ヘッダーファイル tlhelp32.h や winbase.h を参照すると良いでしょう。

さてさて、C/C++ 言語を嗜んだヒトから
ポインタでアクセスしたい番地を指定すればメモリの読み書きが出来るのでは !?
との声も聞こえてきそうです・・・たしかに、不可能とは言いませんが危険極まりない・・・
そもそも、たいてい C/C++ 言語の教本等で扱っているのは単一のアプリ作りと、その範囲内でのメモリの読み書き。ここで行いたいのは複数の異なるアプリ間でのメモリの読み書き。
自宅にある冷蔵庫の中を確認するのと、他人様のお宅にお邪魔して冷蔵庫の中を拝見させていただく位話が違う・・・
ほかにも、
Word や Excel で文字や図形などのデータを共有できる
などの声も聞こえてきそうです。これは、OLE と呼ばれるデータ共有の仕組みであって、両アプリがデータの共有すること前提に成り立っています・・・

C/C++ 言語の教本などに例題としてファイルの読み書きが登場します。おおまかに、ファイルポインタを取得、ファイルの読み書き、ファイルポインタを解放といった具合。
Windows 用のアプリを作る場合、ファイルポインタではなくファイルハンドルになります。つまり、ファイルハンドルを取得、ファイルハンドルを経由してファイルを読み書き、ハンドルを閉じるとなります。
その手順が飲み込めていれば、ファイルハンドルをプロセスのハンドル、ファイルをプロセスの使用しているメモリ領域に置き換えるだけで、手順を飲み込めることでしょう。

「~のスナップショットを作成」という部分が判り難いかもしれません。これは、現在 OS 上で動いているアプリの一覧表を作成することです。
関連する関数名が ~~First や ~~Next となっています。C/C++ 言語を嗜んだヒトならばお気づきのように、ファイル検索の際 _findfirst と _findnext 関数を用いるのと同様です。目的のプロセスと一致するまで while 文 や do-while 構文で繰り返し列挙してゆきます。
後はファイルの読み書きと同じような要領でメモリの読み書きを行います。
ファイルの読み書きと同様、ハンドルの取得に失敗していればメモリの読み書きは行えません・・・

参考までに記しておきます。Windows 用のアプリを作るならば、ファイル検索に用いる関数は _findfirst や _findnext 関数よりも OS 付属の FindFirstFileEx や FindNextFile 関数を利用する方が無難です・・・

・・・嵐世記の話にもどります・・・と、ここまで手間が掛かることを考えると、パワーアップキットに付属する武将データの編集機能で若干修正を加えるにしろ、いかに速やかに進めるか考えた方が有効です。

たしかに、「急いでクリアしたい」や「次のシナリオへ一刻も早く進みたい」と考えがちです。しかし、急いだ分その作品の持つ醍醐味を味わう機会を失います。

先に挙げた「鉄砲を三千挺を集める」や「大砲を15門集める」といった課題であっても地道に進めば解けるように練られています。
もし、難しいなら各月始めに軍団の配分、奉行の割り当てが適切か、出陣前の訓練が十分か、負傷兵の確認などが必要でしょう。戦毎に負傷兵数が激しいならば「合戦中の陣形や敵将への向い方を変えてみる。」、「負傷兵が回復するには3ヶ月ほど要するので、その間は出陣を控える。」など今までと異なる策を試みることが状況改善のきっかけになるハズ・・・

せっかく購入なされた作品、奥深さを味わうのもこれまた然り・・・

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

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

テーマ : PCゲーム
ジャンル : ゲーム

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

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

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

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

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

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