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

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


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



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

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

スポンサーサイト

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

[未掲載分] 「除算が遅い」の補足 (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ビット版アプリを前提とした、レジスタが足りない場合のお話は続きがあります。

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

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

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

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

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

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

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

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

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

最新記事
カレンダー
04 | 2017/05 | 06
- 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 31 - - -
カテゴリ
ランキング
いつも応援いただきありがとうございました。ただいま休養中につきランキングへ参加していません・・・

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

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

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