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

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


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



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

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

スポンサーサイト

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

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

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



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


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


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

この種のお題、鋭いヒトを対象に限定して綴るならば1話簡潔型に進めることも可能。・・・当ブログにたびたびお越しいただいたブロガーさんの中で、プログラミング技術の向上を目指している方々もちらほらと。中でも、学生の方々、学業の合間をみて、向上しようという意欲に心を動かされた・・・というのがこのお題の発端である。
さらに、この題目のきっかけとなったブロガーさんを見るかぎり、「まだ実戦ではなく、C/C++ 言語の習得に奮闘している段階」とのこと。たしかに、不慣れなヒトがつまづきそうな点は多々あり、ひとつひとつ綴ってゆくとかなりの分量となる。どこまでも綴るのは不可能なので、今回をもって一応のピリオドとしたい。

さてさて、Visual Studio を起動してからここまで小一時間。
「高速化のために改良したい」と考えるのは結構なことだが、それに膨大な時間を費したのでは本末転倒。
そもそも、記事にまとめるコト自体が時間のロスと言われそう・・・

環境など
OS は Windows 7 x64、CPU は Core i3 3220T、メモリは DDR3-1333 8GB (4GB 2枚) を搭載した PC 。アプリの作成に関しては、C/C++ 言語、Visual Studio 2008 を用いて x64 向けにビルドした。

筆者がこれを下書きしたのは2012 年暮れのこと。店頭に並んでいる PC を見るかぎり、Intel 製の CPU は「第 3世代 Intel Coreプロセッサー」( 通称 Ivy Bridge ) が一般的であった。なかでも、筆者が所有していた CPU は Core i7-3770 、同 3770T と i3-3220 、同 3220 T 。キャッシュやターボブーストが効かないほうが中立的な結果をえられると考え、i3-3220T を選択した。

車輪の再発明 (6)の終盤で触れたように、結果がバラつく要因を減らしたい。よって、OS を再起動後、省電力状態に突入しない段階で速度を測った。ひとつのパターンにつき10回行った平均値とした。また、極端に値が偏った場合は除いた。

結果を画像として載せる。多少見辛いかもしれない。結果を一覧表としてアップしたほうが読み易い。
以前、別のテーマにて細かい件を綴ったことがあった。結果として、筆者の許可なく転用され、悪用された。
誰しもピックアップされることは嬉しい。反面、濫りに用いられるのは不愉快でもある・・・



右側の列はパターン、手法などを記した。左側の列、縦方向は要した時間。単位はミリ秒。1000 ミリ秒で1秒。
要した時間の中には、残り回数の減算とゼロ確認、ポインタを加算、繰り返しの先頭へのジャンプが含まれている。
ロード、ストア部だけの純粋な数値ではないことに用心いただきたい。

コード等は車輪の再発明 (9)

1番目は OS に備っているメモリ転送関数を呼び出すパターン。
2番目以降は 32 ビット幅や 64 ビット幅の汎用レジスタにロードされた値がストアされる命令が生成されるように手を加えたパターン。

なお、アプリをビルドするにあたり、ターゲットを x64 (64ビット版向け)に設定している。車輪の再発明 (9)で触れた通り、32ビット版をターゲットにビルドした場合、32ビット幅を持つレジスタへのロード、ストアを2回行うように翻訳されてしまう。

汎用レジスタにロードされた値がストアされる命令が生成されたことをアセンブリ言語で出力されたファイルで確認した。アセンブリファイルを出力する方法については車輪の再発明 (7)で触れた。

ほか、ループ ( 繰り返し ) 部を
・特定の回数に達するまで繰り返す
・残りがゼロになるまで繰り返す
と変えたり、複数の汎用レジスタを用いてのロード、ストアを試みた。

ループ数の大小、つまり「何回繰り返すか?」といった部分の差は車輪の再発明 (10)で触れたように、「複数の実行ユニット」で分散する仕組みにより吸収されているようだ。
ストアと並行して残り回数の減算や確認を行っていると推測できる。

この段階で論じるならば、高速化を期待して手を加えたにも関わらず、遅くなるパターンも見受けられる。
ゆえに、OS に標準で備わっているメモリ管理系の関数や古典的な関数を用いるのが無難。

たしかに、昨今でも
「関数呼び出しはオーバーヘッドのため遅くなる。」
等の文言が唱えられている。
オーバーヘッドという表現が難しいだろうか。スポーツの分野では宙返りしながら空中のボールを蹴るコトを指したり、音を楽しむ場ではヘッドフォンを指す。ここでは、ある関数を呼び出す、その中から別の関数を呼び出すこと意図している。
例えば、実生活の場において何らか作業を依頼する場合、元請け・下請け・孫請け・ひ孫請け・やしゃご請け ~~ といった事態に陥ることがある。
依頼者からみれば、丸投げ、中抜きされた分だけ余分なコストが生じる。もし、作業にあたるヒトに直接頼むことができれば「余分なコスト」を省ける。「余分なコストを省きたい」と考えるのは自然なことである。

多段に渡る呼び出しはオーバーヘッド、つまり余分な手間、コストがかかると考えられている。
今回測った数値をみるかぎり、
関数呼び出しに要する余分なコストを差し引いてもOS に備わっているメモリ管理系の関数や古典的な関数速くなることを期待して手を加えたつもりコードとの差は微々たるもの。

次に、SSE2 命令を使うように手を加えた諸々のパターン。


コード等は車輪の再発明 (10)

1番目と2番目はロード、ストアの対象アドレスが16バイトの倍数でなくともエラーが生じないように、C/C++ 言語の組み込み関数として _mm_loadu_si128_mm_storeu_si128を用いた。
16バイトの整列、非整列に関しては車輪の再発明 (8)で触れた。
コンパイル時に出力されたアセンブリ言語のファイルを確認すれば ロードとストアの部分が MOVDQU

1番目のパターンはひとつの XMM レジスタでロード、ストアを繰り返す。
2番目は4本の XMM レジスタを用いてロード、ストアと変更した。繰り返し回数は 1/4に減る。

3番目は ロード、ストア に _mm_load_si128_mm_store_si128を用いた。
アセンブリ言語で出力されたファイルを確認すると、ロードとストアが MOVDQU から MOVDQA に変わる。

4番目、5番目 はストアをキャッシュを介さないように変更。_mm_store_si128の部分を _mm_stream_si128に変更した。
アセンブリ言語でのロードとストアが MOVDQA から MOVNTDQA に変わる。
4番目は4本の XMM レジスタを使うのに対し、5番目は8本の XMM レジスタを使う。

把握し辛いかもしれないので、簡単な棒グラフで示す。





SSE2 を使うように手を加えた段階でも、パターン1からパターン3は、SSE2 命令を使わないパターンや古典的な関数を用いた場合と大差ないように見える。
アドレスが16バイトに揃った状況ゆえに、MOVDQU と MOVDQA どちらも似通った結果が出た。
明らかに向上したのはパターン4とパターン5で、これらはストアをキャッシュを介さないに変更した効果と考えてよさそうだ。
ちなみに、車輪の再発明 (9)で触れたように、SSE4.1 以降では ロードに関してもキャッシュを介さない命令が加わっている。
※ SSE4.1 の命令が使えるのは 第2世代 の Core2 Duo , Core2 Quad 以降。

局所的に使う XMM レジスタの本数を増やす効果はどうだろう。
1本から4本に増やした場合、わずかながら効果があるようだ。一方、4本から8本に増やしても効果が無いようだ。
これは伝送路、通路や廊下が詰まってしまい効果が得られないと考えられる。むしろ、混雑により待たされるかもしれない。混んでいる間に別のコトを進める方が有利ということだ・・・

効果はいかに!?

OS に備わっている CopyMemory 関数を使う場合と手を加えた場合の差は 35 から 38 %。
この数字を見るかぎり、細工により高速化したかという面で疑問・・・



筆者は以前も同じような試みをした。Pentium 4 が全盛の頃、Core 2 Quad / Core 2 Duo が登場した頃、そして今回。

Pentium 4 が全盛の頃、ストリーミング・ストアとソフトウェア・プリフェッチをうまく組み合わせることで4倍から5倍、もしくは、それ以上の高速化を見込めた。
結果を図表に載せていないが、今回もストリーミング・ストアとソフトウェア・プリフェッチの組み合わせも試した。しかし、大幅な速度向上とはならなかった。むしろ、プリフェッチ命令が挿入された分、遅れるようにも感じる。
もしかすると、筆者の組み方が悪いのかもしれない・・・

違う視点から考えてみよう。
「Pentium 4 の頃と比べ相対的な数値が伸びなかった」
という点は否めない。一方、同じクロックの CPU として比べれば
「絶対的な数値は大幅に向上している」と解釈できる。

Pentium 4 ( Prescott 世代 ) と同じクロックの Core i3-3220 ( Ivy Bridge 世代 ) で同じベンチマークテストを行ってみた。
おおむね、メモリ操作を中心としたもので 3.5 倍、シングルスレッドの演算で約 3 倍、マルチスレッドでは 6 倍から8 倍ほどの差が出た。
大雑把に言えば基礎体力が違う。別の言い方をすれば、フットワークが軽くなっている。

フットワークの差を単純に3倍と捉えて考えてみよう。
不慣れな作業員とベテランの作業員がいるとして、同じ時間でこなせる仕事量が3倍違うと仮定する。
前者に、もともと60分かかる作業を任されたとする。本気で挑み15分で終わるようになると、かなり短縮されたと実感できる。本気で作業するにあたり、細々と指示が与えられた。
一方、後者は基礎的な力が3倍。ということは要する時間が1/3。前者が60分かかる作業、細々と指示を与えずとも20分で完了するだろうと期待される。こちらも本気で挑むとして、作業時間が20分から15分や10分と短くなったとしよう。相対的な評価となれば「かなり短縮された」とは扱われないものだ。
とはいえ、無限に短縮できるものではない。どのような分野でも超えられないカベがある。
要は伸び代がある、ない。この例で、不慣れな作業員は伸び代があるに対しベテランの作業員は伸び代がないと言える。

技術競争が活発な分野では、現状の弱点を克服しようと常に改良・改善が施される。また、新しい技法も編み出される・・・

アプリ作成、とりわけ速度に話を絞ろう。
旧来はソフトウェアレベルで人間があれこれ指示する必要があった。やがて、ハードウェアレベルで進化したことにより、細々と指示が与えなくともよくなった。
と言える。たしかに、
「現時点で遅いから何とかしたい」という希望を抱くのは良いことだ。
もちろん、ハードウェアレベルでの改良作業に携わっているならば・・・
しかし、ソフトウェアレベル、というよりアプリを組む側ならば
「現在の弱点は将来、機材の進化で解消されるかもしれない。」
と割り切って考えるべきだろう。

実際、永く続いているアプリはいくつものバージョンがリリースされてきた。機材の世代に応じた最適化が施されてきた。偏った最適化の弊害として、機材の世代によっては能力を発揮できないかもしれない。
商用としては、それも一理ある。新世代向けの新製品をリリースすれば、消費者の購買意欲を高めることができるであろう。

仮に、アプリ作成に不慣れな段階であっても、「最適化」や「高速化」に興味を持つのは多いに結構。それらを扱った文献も多い。しかし、優先すべき課題は、完走すること。ゴールまでたどり着くことだ。
ゴールと表現しているのは、アプリ、つまり作品を完成させることではない。
「実行中に暴走したり、リタイヤするような、残念なアプリを組まない」ことを目標に進むべきではないか・・・

ここから先、載せるべきか迷った。この題目のきっかけとなったブロガーさんは、2012年暮れの段階で Windows XP を利用しているとのこと。「32ビット版 のプラットフォームを利用しているヒトはまだまだいる」との判断から載せることにする。あくまでも参考程度・・・

プラットフォームを x86 、つまり 32ビット版の Windows 向けに切り替えた結果は以下の通り。



ほぼ、64ビット版のプラットフォームで試した際と同じ傾向であった。興味深い点もちらほらと。

まず、MOVSD を用いた場合の結果は 478。車輪の再発明 (9)の終盤で触れたように MOVSD は複数の意味があり、ここでは Move data from String to string 「ストリングの移動」を指す。
32ビット版の CopyMemory 関数を呼び出す場合より遅くならない、というより 15% ほど速い

PC の中ではデータの移動やコピーは頻繁に行われる。これが短縮されることは大多数のヒトにとってメリットである。
かつては、メモリ間コピーを高速化しようとさまざまな技法が提唱、編み出されてきた。なかには、頭の体操というレベルを超え、アクロバティックな技法も存在した。
逆に言えば、メモリ間コピーが速ければこの種の試みは避けられていただろう。同様に筆者もMOVSD や MOVSB , MOVSW は速くないと思い込んでいた。昨今、MOVSD を用いても遅くならないようにハードウェアレベルで進化したと解釈できる。

ほかにも、32ビット版 に限られてしまう手法としては MMX レジスタを用いてキャッシュを介しないデータ移動が考えられる。こちらは 25 % ほど速い。かつて、2倍近く高速化される例も体験したことがある。その頃に比べ相対的な伸び代がないのかも。
ただし、高速化が見込めそうだというだけで期待しないほうがよい。なぜならば、64ビット版の Windows では FPU や MMX の利用がサポートされない。
※ 浮動小数演算を高速化するための FPU と MMX は表裏一体。

SSE 本来の使い道!?

最後に、誤解がないよう加えておく。今回の記事は 「2012年の暮れ時点でロードやストアの高速化に適さなかった」を旨としている。決して、SSE や AVX などの存在意義を否定するものではない。
車輪の再発明 (2)で述べた通り、SSE や AVX などの機能が追加された経緯としては、画像や音声などのデータ処理を効率的に行うためである。
SSE や AVX などの機能は、ロードやストアの高速化を目的に拡張されたのではない。画像や音声データを加工処理する際、同じような演算が繰り返される。これを同時に処理することで時間の短縮を狙いたい場合に適している。

ロード、演算、ストアをそれぞれ1ステップとするならば、旧来からの方法は4回繰り返せば 3ステップの4回分、12ステップ。
※ 必要なクロック数ではない。演算にかかるクロック数に興味があるヒトは2012年 8月13日掲載の除算が遅いをどうぞ。

SSE/SSE2/SSE4 などを用いれば、同時に4つ分をロード、同時に4つ分の演算、4つ分ストアの3ステップに減る。
ロードとストアに要する短縮は期待できないとしても演算3回分のステップが減り、4+1+4 、それ以前と比べ高速化が期待できる・・・

判り辛いだろうか。2012年 8月13日に掲載した記事のようにスイカを4等分する例で考えてみよう。



目の前にたくさんのスイカがあるとする。
今までの4倍、まな板の上にスイカを移動する。4等分したスイカをまな板の上から皿に移す。この辺の移動に要する時間はさほど変わらない。
まな板の上でスイカを切る、割る作業を担当するヒトを今までの4倍に増やす。それまでは一人で4等分していたとすれば、4人で同時に作業にあたる。よって、まな板の上で行う作業が4倍速くなる、作業時間が短縮される・・・のように想像すれば並列に実行するメリットが判るハズ・・・
スポンサーサイト

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

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

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

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

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



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


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


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

前回までで、旧来からの4バイトずつ転送する例を触れた。今回は、前回までの流れの続きとして、SSE2 を使って 16バイトずつ転送する例へと進む。

最終的に知りたい点は、「一度に運べる量が今までの4倍」に増えれば「作業速度も4倍」に向上するか否か。
唐突にこの話題に進んでも伝え難い。詳しくは車輪の再発明 (2)をご覧あれ。



Intel 製の 32ビット CPU に絞って遡れば、1980年代に普及した 80386 をベースに MMX や SSE , SSE2/SSE4/AVX といった機能が拡張されてきた。
画像や音声の処理の分野において、もともと搭載されている8本の汎用レジスタだけでは速度的に不利な局面があり、これを解消しようと並列化、簡単にいえば、ひとつの命令で同時に複数の演算を実行できるように改良されてきた。
MMX 機能が追加された際は新たにレジスタが追加されたワケではなく、浮動小数演算用のレジスタが空いている時に MMX 命令を実行するようになっていた。
MMX は整数演算処理に限られていた。やがて、浮動小数点演算を高速に処理すべくSSE 機能が追加された。この際、XMM レジスタと呼ばれる128ビット幅のレジスタが8本追加された。
C/C++ 言語でいうところの float 型 、 32ビット幅の浮動小数値4つ分を同時に演算できるようになった。浮動小数を現す変数の型として倍精度の double という型があり、単精度の float 型よりも精度の高い結果を得られる。というよりも、より誤差が少ないと言ったほうが近いだろうか。
精度を重要視する際、小数・実数を扱う型としては 80ビット幅もありうるが昨今は 64ビット幅である。
SSE から SSE2 へ拡張される際、XMM レジスタで64ビット幅の浮動小数値2つ分を同時に演算できるようになった。ほかにも、32ビット幅の整数4つ分や64ビット幅の整数2つ分、さらには16ビット幅の整数を同時に8つ分演算できるようになった。

CPU の64ビット化にともない、汎用レジスタとXMM レジスタは8本から16本へと増えた。実際には 関数呼び出しで4本まで使う決まりになっているので、自由に使えるレジスタの数は 12本・・・

もちろん、MMX や SSE が登場する前から、並列化に関しても取り組みが行われてきた。「複数の実行ユニットを並列に動作させることで命令を並列的に実行させる」仕組みが強化されてきた。
「並列」や「命令の依存関係」と表現すると難しいだろうか。「互いに無関係な処理を同時に実行する」ための仕組みと言い換えることができる。


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

かつて、過去記事手分けすれば速く済む?で似たような話を述べた気もしますが、想像し辛いかも・・・

例えば、倉庫を掃除するとして、右端から左端まで約1時間掛かる作業だとしよう。まずは、コスト ( 人件費 ) の増減や物資の限定 ( リソースを共有する ) を考えないものとする。
「複数の実行ユニット」と言う表現では話が複雑になりそうなので、実行ユニットを2つと仮定した場合2人目の作業員を増やす、もしくは2つの班に分けて清掃に当たるのと等しい。
互いの作業に依存性がないならば、30分程で完了に至ることだろう。さらに実行ユニットが4つに増えると作業完了までに要する時間を15分くらいまで短縮できるかも。
「作業に依存性がある・ない」の部分が判り辛いかもしれない。リソース ( ここでは作業に必要な道具 ) を共有するという条件が加われば、他の作業員が道具を使い終わるまで待たされる。待ち時間が生じる、イコール、時間の短縮とはかけ離れてしまう。
実際何かを作ることを想定して分業するならば、ひとつ目のユニットが原料を運び込み、ふたつ目のユニットが原料を刻むなどの加工 ~~ 、n番目のユニットがパッケージに詰めるといった具合になる。前のユニットの結果を待って次のユニットが動きだすことは「作業に依存性がある」と言える・・・
効率的な作業もしくは非効率に陥るのかは条件だけでなく、命令の出し方・順序によって左右される。
この辺は人間が手動でアレコレ迷うよりも、コンパイラに任せてしまうのが賢明だ。
開発環境のバージョンが新しくなるごとにコンパイラが賢くなっており、最適化、つまり、より効率的な命令順序の組み合わせが生成されるようになってきている。

コードを載せる前にもうひとつ。開発環境について。
ビルド、実行するには
開発環境が SSE2 の命令に対応しているほか 、OS レベルでのサポートが必要になる。

車輪の再発明 (3)で述べた通り、開発環境として Visual Studio の利用を想定している。
Visual Studio で SSE2 以降の命令セットが使えるのは Visual Studio .NET や Visual Studio 2003 以降。SSE4 以降の命令セットを使いたいならば、Visual Studio 2008 以降。

少し古い Visual Studio 6.0 でも Processor Pack を導入することでSSE2 命令の利用が可能になる。しかし、それ以降の Visual Studio に比べ SSE2 を使った場合に良好なコードが生成されない、最終的に提供された不具合修正は Service Pack 6 だが Processor Pack が正式にサポートされているのは Service Pack 5 まで、などなど面倒。
ちなみに、「Visual Studio 6 無料」などの検索語彙で訪れるヒトがいるので記しておきます。Visual Studio 6.0 に無料版はありません。 ( 学生さん向けの格安パッケージは流通していました。)
Visual Studio の中で Express Edition と呼ばれる無償で提供されるエディションが登場したのは Visual Studio 2005 から・・・

SSE2 命令など後から追加された機能を使う際は、OS レベルでのサポートされているか否かも重要。
具体的な対処法としては、アプリ起動直後に IsProcessorFeaturePresent 関数を呼ぶ。SSE2 のサポートを調べるには PF_XMMI64_INSTRUCTIONS_AVAILABLE を指定する。古い開発環境ではこの値が未定義かもしれない。関数を呼ぶ際、直接値を指定するなら引数は 10。同様に SSE3 のサポートを確認するには PF_SSE3_INSTRUCTIONS_AVAILABLE 、もしくは 13を指定すると良い。
ちなみに、筆者の手元では Windows 2000 や Windows XP で SSE2 命令を駆使したアプリを使ってきたが、致命的なエラーに至ったことはない。
さらに、SSE4 以降の新しい命令体系ということで挙げると、第2世代 Core i7 シリーズ、いわゆる Sandy bridge で「AVX」が追加された。その後第4世代 Core i7 シリーズ、いわゆる Haswell から「AVX2」が追加。
これらを使いたい場合、OS レベルでのサポート は Windows 7 SP1 または Windows 8 以降。SP1 を適用していない Windows 7 で AVX / AVX2 命令を用いたアプリを使った場合、何が起こるかわからない・・・

前回とりあげたような、4バイトずつ転送する部分を SSE2 命令を用いるように変えると以下のような感じ。
__m128i *pdqd , *pdqs;
pdqs = (__m128i *)(lpTemp[2]);
pdqd = (__m128i *)(lpTemp[3]);
SIZE_T j;
for(j = (__SIZE_DUPLICATE / sizeof(__m128i) ) ; j ; -- j){
    _mm_store_si128(pdqd , _mm_load_si128( pdqs ) );
    pdqs += 1;
    pdqd += 1;
}

__m128iの部分は変数の型。先に述べた SSE に対応した際に増設された 128ビット幅、つまり 16バイト分幅のXMM レジスタを使うことを意味する。
SSE2 より前、初期のSSE では 32ビット幅の浮動小数値を扱う段階では__m128であった。SSE2 から 整数演算 や 64ビット幅の浮動小数値の演算が可能になった。XMM レジスタを整数演算で使う場合は__m128i、64ビット幅の浮動小数の演算・実数の演算では__m128dを使うようになっている。
128ビット幅の整数を扱いたいならば UINT128、__int128、int128_t なども挙げられる。しかし、これを書いた時点で Intel 製の CPU を搭載した一般的な PC を想定するかぎり、128ビット幅の汎用レジスタは装備されていない。
よって、XMM レジスタを経由したデータの読み書きを試みる。

変数名は前回と同様ポインタの「p」、ダブルクワッドワードの「dq」をつけた。
前回、DWORD の意味として Double WORD 、2つ分の16ビット幅の符号なし整数と述べた。クワッドワード は Quad WORD、16ビット幅4つ分で64ビット幅。ゆえに、ダブルクワッドワード は Double Quad WORD、16ビット幅8つ分の128ビット幅を指す。ほかにも 128ビット幅を現す Octaword なども考えられるがお目にかからない。
「pows」と「powd」では「pow」べき乗を求める pow 関数と間違えそう、「pm128is」「pm128id」や「pxmms」と「pxmmd」の組み合わせ判り辛い。
今回は単純な例なので、変数名は深く考えず「ps」と「pd」の組み合わせでもよいだろう。

反復部分は for 文を用いるか while 文を用いるか好みの問題になりそうだが、ループの制御条件などをひとまとめに書き易いので for 文を用いた。
繰り返す回数は __SIZE_DUPLICATE として定義した数の1/16。4バイト分ずつ転送するのに比べ、回数は 1/4に減る。
なお、反復する内容がひとつの文で書ける場合、ブロックを波括弧の記号で囲む必要はない。今回はひとつの文で書ける部分を複数命令に分けたためブロックを波括弧の記号で囲んだ。

_mm_store_si128_mm_load_si128は書き込み( = ストア) 、読み出し( = ロード)。
pdqs の指すアドレスから16バイト分のデータを読み出し、pdqd の指すアドレスへ16バイト分のデータを書き込む。
車輪の再発明 (6)にて SSE 命令を使う際の注意点として「アドレスを 16 の倍数に整える」ことを述べた。16 の倍数に整っていない場所のデータの読み書きは致命的なエラーや異常終了を引き起こす。
もし、16 の倍数に整っているとは限らない場所への読み書きを行うことが避けられないならば、速度向上は問えないが、
_mm_storeu_si128_mm_loadu_si128などの「u」が付いている命令に置き換えるのが無難。
上の例を「u」が付いている命令に置き換えるなら
_mm_storeu_si128(pdqd , _mm_loadu_si128( pdqs ) );
となる。

pdqs += 1;pdqd += 1;の部分でポインタ ( 読み出し、書き込み各々アドレス )を増やす。
細かく言えば、確保したメモリ領域が連続していて低い番地から高い番地へデータへとデータが順々に続いていることが前提。
ポインタの扱いについて稀に勘違いするヒトがいるので記しておきたい。
前回触れたように、△ +=1△ = △+1と同等。
整数の演算を念頭に △ +=1を用いるならば、単純にプラス1の足し算。元の値が400ならば演算結果は401。
ポインタの演算で △ +=1 を用いた場合はプラス1 ( 1バイト増える ) ではない。データ型のサイズ分増える。

ここで言う「データ型のサイズ」とは変数の型の大きさや構造体の大きさを指す。「型」は「値の意味」ともとらえられる。
データ値の意味する大きさは何バイト分なのかを考えると理解が早まるハズ。
例えば、前回とりあげた DWORD32 は32ビット幅の符号なし整数。データ型のサイズは4バイト分。よって元の値が400ならば演算結果は404。
今回とりあげた __m128i は 128ビット幅、バイトに変換すると16バイト分。よって元の値が400ならば演算結果は416。

C/C++ 言語で「変数のサイズは?」「その型は何バイト?」を知るには sizeof 演算子を用いると楽。今回の例でも反復回数を指定する部分でsizeof( 型 )を用いた。
j = (__SIZE_DUPLICATE / sizeof(__m128i) ) ; と書いた部分は j = 全体のバイト数 ÷ 16 と解釈できるハズ・・・

さてさて、この辺で速度比較といきたいところだがもう少し手を加えてみよう。

「16バイト分ずつ読んで、書いて ~~」の仕組みを「16バイト分ずつ4回連続して読み、16バイト分ずつ4回連続して書き出す ~~」に変えてみる。
繰り返す回数は __SIZE_DUPLICATE として定義した数の1/64。4バイト分ずつ転送するのに比べ、回数は 1/16に減る。

__m128i *pdqd , *pdqs;
pdqs = (__m128i *)(lpTemp[2]);
pdqd = (__m128i *)(lpTemp[3]);
pdqd -= 4;

SIZE_T j;
for(j = (__SIZE_DUPLICATE / (sizeof(__m128i) * 4) ) ; j ; -- j){
    __m128i xmm[4];

    xmm[0] = _mm_load_si128( (__m128i *) pdqs);
    xmm[1] = _mm_load_si128( (__m128i *) (pdqs + 1) );
    pdqd += 4;

    xmm[2] = _mm_load_si128( (__m128i *) (pdqs + 2) );
    xmm[3] = _mm_load_si128( (__m128i *) (pdqs + 3) );

    _mm_store_si128((pdqd + 0), xmm[0]);
    _mm_store_si128((pdqd + 1), xmm[1]);
    pdqs += 4;

    _mm_store_si128((pdqd + 2), xmm[2]);
    _mm_store_si128((pdqd + 3), xmm[3]);
}

__m128i xmm[4];の部分は XMM レジスタを4本使うための宣言。筆者もいろいろ試してきたのだが、最終的にはこの書き方に落ち着いた。
一見、この方法ではメモリの一部に64バイト分の領域が確保されて、ロードの度にXMM レジスタの内容を xmm[0] から xmm[3] 一時的にストア、一時的に置いた内容をさらにロードしストア用ポインタの示すアドレスへストアするといった勘違いが生じるかも。
実際は、xmm[0] から xmm[3] は局所変数で寿命は for 文の波括弧が閉じられる部分まで。
コンパイラが翻訳時に適切な判断を下せば、XMM レジスタ の空きに余裕があると判断できるハズ。よって、一時的な変数領域へのストア、ロードは省かれる。

最近の開発環境を利用しているのであれば、わざわざ配列変数として宣言する必要はない。
宣言部分は__m128i xmm0, xmm1, xmm2, xmm3;とし、ロードはxmm0 = _mm_load_si128( ~など、添え字の無い変数を使っても4本の XMM レジスタを使うコードが生成される。
筆者の体験した例では、古めの開発環境で配列変数を使うようにしないと4本の XMM レジスタが使われず、3本もしくは2本の XMM を使ったコードが生成されることがあった・・・

冒頭で述べた、「命令を並列に実行できるか否か」に関しても違いが出そうだ。
手を加える前のソースコードと手を加えた後のソースコードでどのように翻訳されたのか、違いを把握するには、出力されたアセンブリ言語のファイルを比べると判り易い。

反復作業において、「残り回数を1つ減らす」と「残り回数はいくつ?」「残り回数がゼロでないならば、繰り返しの先頭へ移動」の命令が遂行される。これらの作業には依存性がある。「残り回数」を共有している。
複数の実行ユニットがあるならば、2番目の実行ユニットを待たせないように2つの命令の間に別の命令を挟むと良い。

まずは、最初の SSE2 命令を用いた最初のコード。for 文によるループの内側をアセンブリ言語で見てみよう。
Visual Studio を利用しているとして、アセンブリ言語ファイルを出力する方法は車輪の再発明 (7)で触れた。



上の図、C/C++ 言語でfor(~~ ; j ; -- j)に該当する「条件判定」、「繰り返すか否か」はどのように翻訳されたのだろうか。
sub r9, 1 が「残り回数を1つ減らす」、
jne SHORT $LL~~ が「残り回数がゼロでないならば、繰り返しの先頭へ移動」に該当する。
最初のソースコードから生成されたアセンブリ言語コードでは「XMM レジスタをストアする」が間に入っているのがみてとれる。
movdqa 命令が2つ登場している。見分け方は
movdqa xmm△ , XMMWORD PTR [ ~~ ]の方はロード、
movdqa XMMWORD PTR [ ~~ ] , xmm△の方はストアと考えると良い。

ところで、「残り回数はいくつ?」に該当する部分が省かれている点に気が付いたヒトもいるだろう。
減算にともない、自動的に値がゼロか否かチェックする仕組みとなっている。よって、省いても構わない。
省略しないで書きたいならば、jne SHORT $LL~~の前に、test r9, r9もしくはcmp r9, 0等の「ゼロとの比較」を意味する比較演算命令を追加すると良いだろう・・・

SSE2 を用いたコードで手を加えた後、出力されたアセンブリ言語ファイルは次の図。



手を加えた方の例を見ても
「残り回数を1つ減らす」と「残り回数がゼロでないならば、繰り返しの先頭へ移動」の命令の間に「3本目のXMM レジスタをストアする」と「4本目のXMM レジスタをストアする」命令が並んでいる
ほかにも、ループ冒頭でストアするポインタを増加させている部分など、4回連続でロードを行う合間に、ストアするポインタを増加させている。依存性のない命令順序に並ぶことを狙っていたが、人間の狙いに近いコードが生成されている。
最初の例では、ロード・ストアの直後にポインタを増加するようになっていた。依存性がある命令が続いている状態では待ちが生じる可能性が高い・・・

ところで、「プリフェッチ命令は不要???」との声も聞こえてきそうだ。既に車輪の再発明 (1)で述べたように、Pentium4 全盛時代、プリフェッチ命令を追加することで先読みによる効果を期待できた。
Core2 Duo , Quad やそれ以降に登場した Core i7 シリーズなどは機械内部で先読みを行うように強化されてきた。旧来のようにプリフェッチ命令を加えて先読みによる速度向上を期待しても、昨今の PC では余分な作業が1ステップ増えるだけで速度は同等か場合によっては遅延を招く。よって、今回はプリフェッチの追加については扱わない。
かつて、全てアセンブリ言語、多少複雑なコードという条件で、プリフェッチとストリーミングストアを組み合わせたことで転送速度を大幅に向上させた経験がある。ただし、お手軽ではない。この辺は機会があればいずれ・・・

さてさて、冒頭で述べたように XMM レジスタは16本 ( 32ビット環境では8本 ) ある。
そこで、「もっと多くのXMM レジスタを使うと良いのでは!?!?」という声も聞こえてきそうだ。
まず、関数呼び出しの際、引数を渡すために4本分は使えない。ほかにも、レジスタ間で一時的にコピーが行われることなどもあり、全ての本数を使いきるようなコードは避けたほうが無難。
結論めいたことを先に述べてしまうが、昨今の CPU では局所的に4本以上使っても速度向上が見込めない。実際、8本のXMM レジスタを使う例も試したが、4本使う例と差が無かった。
たしかに、それ以上の本数を使うことで、繰り返し回数を減らすことは出来そうだ。しかし、出力されたアセンブリ言語ファイルから判るように、依存性がある命令が離れて配置されていれば、「残り回数 ~~」を減らすことと速度向上が結びつかないのかも・・・

ほかにも、高速さに結びつきそうな点、データのストア速度が向上する可能性が残っている。
ただし、書き込むアドレスが 16 バイトの倍数に整っていることが必須。

ストア命令_mm_store_si128 の部分を _mm_stream_si128 と書くこともできる。
この置き換えた命令の説明は
「非テンポラルなヒントを使用して、メモリへの書き込み時のキャッシュ汚染を最小限に抑え ~~ 移動する。」
と載っていた。この説明では判り辛い。
「キャッシュを介さずにストアする。ただし、書き込もうとしているアドレスを含むキャッシュラインがすでにキャッシュ内にある場合、キャッシュは更新される。」
この説明でも、表現が硬い。もっと簡単に「なるべくキャッシュを使わないようにデータをストアする。」くらいが判り易い。

キャッシュメモリを更新する作業は時間を要す。キャッシュメモリの役割は2回目以降の読み出しを円滑にする。一般的なアプリでは有意義。
今回のような例では同じアドレスに対し、2回目以降の読み書きは無い。よってキャッシュを介す必要はない・・・



_mm_store_si128 や _mm_stream_si128 のままでは覚え辛い。既に車輪の再発明 (8)で触れたように、
接頭の「_mm」を外し、接尾のデータ型を指す「_si128」を外してしまえば「_store」と「_stream」が残る。
「_mm_store_△△」が一般的なストア命令、それに対して「__mm_stream_△△」のような命令が備わっている場合、速度改善が期待できるかも・・・



たいてい、「__mm_stream_△△」はアセンブリ言語で MOVNT○○ となる。
興味があるヒトは車輪の再発明 (8)で触れた、IA-32 インテル アーキテクチャ ソフトウェア・デベロッパーズ・マニュアル、 中巻Aの命令リファレンスより MOVNT○○ を漁ると理解が深まるだろう。

SSE2 を使うという前提から離れ、SSE4 まで範囲を広げるならば、ロードに関してもキャッシュを介さない命令が存在する。
_mm_load_si128(の部分を _mm_stream_load_si128(と置き換えることで、ロードの速度が向上する可能性がある。

冒頭で述べたことと重複するが、SSE4 以降の命令セットを使ったアプリを作るには、Visual Studio 2008 もしくはそれより新しいバージョンが必要。
SSE4 は SSE4.1 と SSE4.2 に分かれていて、ここでは SSE4.1。SSE4.1 の命令を使うには smmintrin.h というヘッダーファイルをインクルードする。
ヘッダファイルのインクルードについては車輪の再発明 (5)で触れた。
ヘッダーファイル「stdafx.h」の終盤、
#include <emmintrin.h>
の後ろに
#include <smmintrin.h>
を加える。

なお、日本語訳されたIA-32 インテル アーキテクチャ ソフトウェア・デベロッパーズ・マニュアルの表紙には2004年と刻まれている。それ以降に追加された機能などの情報は載っていない。
C/C++ 言語での組み込み命令_mm_stream_load_si128(movntdqaへと翻訳されるのだが、MOVNTDQA に関する記述がない。日本語版ではMOVNTDQの解説の前はMOVMSKPSの情報、次はMOVNTIの情報となっている。
SSE3 が搭載された PC が登場したのは2006年頃、SSE4.1 が搭載された PC が登場したのは2008年頃だったと記憶しているが、それ以降も英語版のインテル アーキテクチャ ソフトウェア・デベロッパーズ・マニュアルは改定が施されている。
タイトルがIntel64 and IA-32 Architectures Software Developer’s Manualへと変更されている。
この記事を下書きした時点では 2010 年、2011 年に改定されたバージョンを発見することができた。それらを眺めたかぎり、「同等のC/C++ コンパイラ組み込み関数」も併記されていた・・・

以下に検索へのリンクを貼っておきます。ヒットした中からインテル公式サイト、PDF ファイル形式に絞るとよいでしょう・・・
インテル アーキテクチャ ソフトウェア・デベロッパーズ・マニュアル 中巻A (英語版) への検索リンク
インテル アーキテクチャ ソフトウェア・デベロッパーズ・マニュアル 中巻B (英語版) への検索リンク

さてさて、残りは速度を測って比較、検証・・・

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

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

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

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

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

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



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


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


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

今回もサンプルコードを載せる。特に難しいところは無いので、サラっと次へ進みたいところ。だが、不慣れなヒトのために少々述べておく。

これを下書きした時点において、これからアプリを作ろうとするヒトが参考にするような書籍や解説系のサイト等は 32ビット環境を主軸に書かれている。
たしかに、Windows 95 が登場した頃からつい数年前までは 32ビット環境が主流であった。Windows Vista の登場により 64ビット環境の構築が容易になったが、店頭販売されていた PC のうち、80 ~ 90%は 32ビット版 の OS を搭載したモデルだったように記憶している。
64 ビット版 の OS が異端というワケではなく、既存のアプリとの互換性、つまり、「昨日まで使ってきたアプリを新しい PC でも使いたい」との声に応えたかったのであろう。
筆者の個人的な感覚で言えば、 Windows 7 の導入を機に 64ビット環境への移行が進んだように感じる。

OS が変わっても昨日までと同じ動作、結果を求めがちだ。既存のアプリとの互換性で言えば、昨日まで使ってきた 32ビット版のアプリがお行儀よく組まれたアプリであれば、64ビット版 の OS 上でほぼ動く。
「互換性が保たれる」という点から甘えが生じる。「このアプリはまだまだ使える」と思い込んでしまえば、新バージョンのアプリへ乗り換えを先送りしがちだ。仮に 32ビット版の新しい OS を選択しても、お行儀の悪いアプリは何かとトラブルのもと。OS を乗り換えたら、アプリも新規に買い揃えるのが無難だろう・・・

話が逸れてきたので戻ろう。
データ幅、レジスタの幅の意識を

CPU のデータ幅やアドレス幅などを論じだすと誤解が生じる可能性がある。ここで述べるのは、数値を扱える幅、データ型のこと。

Windows 用のアプリを作る場合、独特のデータ型の呼び名を覚えることになる。
今回はC/C ++ 言語でアプリを作る例として、DWORD32 や DWORD64 というデータの型を用いる。詳しくは windef.h や basetsd.h という名称のヘッダーファイル中で定義されている。

DWORD32 は32ビット幅の「符号なし整数」。通常は 「32ビット」と指定せずに DWORD と書くことが多いだろう。
C/C++ 言語の入門書等では unsigned int などが載っているハズ。

同様に、64ビット幅の符号なし整数を用いたいならば DWORD64 や DWORDLONG を用いる。
ほかにも、符号なし整数の仲間として UINT16 , UINT32 や UINT64 等のデータの型が定義されている。これらは固定幅の整数型である。

ならば、「16ビット幅の時はDWORD16 ???」
との声も聞こえてきそうだ。
DWORD のもともとの意味は Double WORD。WORD とは16ビット幅の符号なし整数、unsigned short つまり、DWORD は 16ビット幅の符号なし整数2つ分。
16ビット幅の「符号なし整数」をお望みならば 「D」「16」も付けないWORD。
アセンブリ言語では 16ビット幅の符号なし整数4つ分、64ビット幅という意味で QWORD もある。Quad WORD の略。既出の DWORD64 や DWORDLONG よりも QWORD の方が判りような・・・

ほかに「符号なし整数」としてunsigned long が紹介されている例もみかける。たいていは 32ビット幅を指していると思われるが、プログラミングモデルによって64ビット長の符号なし整数を指すケースもあるので要注意。こちらも固定幅の整数型として用いるならば ULONG32 や ULONG64。

前話、車輪の再発明 (8)で「32 ビット版の Windows と 64 ビット版の Windows の両方のポインタに合わせてサイズが変更される整数型」として、UINT_PTR という「符号なし整数」について触れた。
「_PTR」の付く整数型は流動的に翻訳される。ポインタの変換などを扱うのには便利。


今回は 旧来の4バイトずつ転送する例と16バイトずつ転送する例を比べたい。よって、「32ビット幅の~~」と明確に意識したコードを書く。1バイトは 8 ビット、ゆえに 32ビット幅は4バイト幅を指す。

ところで、入門者向けの書籍や解説系のサイト等で扱われている整数のデータ型でもっとも多いのは int。冒頭で述べた、「32ビット環境を主軸に」言い換えれば「32ビット環境を前提に」とも言えるが、モノによっては int は 32ビット幅と決めつけているような例もある。
「int は 固定幅の整数型ではない」という考え方は筆者の思い込みかもしれないので、手元にある K & R 本を確認してみた。それによれば データ型の説明として
・「通常、特定の計算機に自然な大きさ」
・「short は int より長くてはならず」
・「int はlong より長くてはいけない」の3点が挙げられているが、int は 16ビット幅 ( 2バイト ) とも 32ビット幅 ( 4バイト ) とも明言されていない・・・

コードと説明を載せる前にもうひとつ。
データ型を確認する方法を載せておきたい。
データ型を知りたい状況になると、windef.h や basetsd.h などのヘッダーファイルを探し開く。



上の図は Windows 7 でのファイル検索の例。
エクスプローラー右上の検索ボックスを使えば多数のファイルを探し回るよりも楽。



同じような名称のファイルが複数列挙される。更新日時が目安になるかもしれないが、どれがアクティブなのか迷うのも当然。
開発環境として Visula Studio を使っているならばもっと楽になる。

例えば 、先ほど触れた DWORD32 を確認したいとしよう。



ソースコードの調べたい語句をダブルクリックすると選択された状態になる。反転表示される。
ここでキーボードの[F12]ボタンを押せばOK。

マウス主体に操作している時や[F12]ボタンが効かないこともありそうなので加えておこう。
反転表示された真ん中周りへマウスのポインタを移動し右クリック。
キーボードから操作する場合は、[Shift]ボタンを押しながら[F10]を押す、もしくはアプリケーションキーを押す。
※ アプリケーションキーとは右 Ctrl ボタンの近くにあり、メニューとマウスポインタのような矢印が描かれたボタン。



コンテクストメニューの [定義へ移動] もしくは [宣言へ移動] をクリック。



DWORD32unsigned int であることを確認できるハズ。



タブをみると、BaseTsd.h を開いていることが判る。
確認が済み次第ファイルを閉じる。くれぐれも、ヘッダーファイルに変更を加え保存しないように。

ファイルを閉じるにはタブを右クリックしてコンテクストメニューの[閉じる]を選択するか、キーボードの[Ctrl]ボタンを押しながら[F4]を押す。
キーボード操作で間違えやすいのは[Alt]ボタンを押しながら[F4]を押すことによるアプリの終了。ここで言うアプリの終了とは ウィンドウ右上の[閉じる]をクリックするのと同等、Visual Studio の終了・・・

前話の終りで 「CopyMemory 関数 を書き換えてみる」 と述べた。正しくは「自分で書いてみる」という方針。
「書き換え」という語句は API フックを指すこともある。フックとは釣鐘ではなく、関数を横取りすること。筆者もその手法を用いて作ることもあるのだが奥が深く、ここに載せるには長い。そもそも、記事のタイトルと逸れてしまう・・・

ということで、CopyMemory 関数のようなコードを自分で書いてみる。

3話前の車輪の再発明 (6)で触れた
SIZE_T i;
for(i = 10 ; i ; --i){

CopyMemory( ~ );
}

この4行のうち、for( ~ ){} の間、CopyMemory( ~ );を変更。

最初は簡単で手軽に考えてみる。仕組みとしては、あらかじめ決めた回数だけロードとストアを繰り返す。
4バイトずつ転送するので繰り返す回数は __SIZE_DUPLICATE として定義した数の1/4。
DWORD32 *pdwd , *pdws;
pdws = (DWORD32 *)(lpTemp[2]);
pdwd = (DWORD32 *)(lpTemp[3]);
SIZE_T j;
for(j = 0 ; j < (__SIZE_DUPLICATE / sizeof(DWORD32) ) ; j++){pdwd[j] = pdws[j];}
for 文による反復を使ってこんな感じだろうか・・・

#if __MYAPP_LEVEL == 2
// 第2番目のパターンのコード
#elif __MYAPP_LEVEL == 3

2話前の車輪の再発明 (7)で触れた #if ディレクティブ ~~ #else ディレクティブ ~~ #endif ディレクティブを活かし、第2パターンのところにコードを追加する。

慣れないヒトにとっては変数名が気になることだろう。
ハンガリアン記法ハンガリー表記法と呼ばれ、古くから Windows でプログラミングする際に用いられてきた。
プレフィックス、接頭辞に特徴があり、ポインタであれば「p」、データ型が DWORD ならば「dw」といった具合。
※ 「用いられてきた」と過去形で表現は、昨今の MSDN ライブラリなど、変数や関数の名前付けガイドラインに「ハンガリー表記法は使用しないでください」と記述があるから。

DWORD32 *pdwd , *pdws;
の部分は「32ビット幅の符号なし整数へのポインタを使う」
pdwd[j] = pdws[j];の部分で
ポインタ pdws の指すアドレス (= 場所) にある数値を4バイト分(= 32ビット分) 読み出し、ポインタ pdwd の指すアドレスを4バイト分書き込む。
それを for 文で繰り返す。転送するバイト数は既に __SIZE_DUPLICATE で定義している。この数を 4バイト分 で割った回数繰り返す。

本来、for 文や while 文、さらには if 文において単一の命令を繰り返すならば、命令を {} で囲む必要はない。
入門用のサンプルでは略されることが多く、{} を省いた方が見た目がシンプルである。
実際には繰り返し文の中に複数の命令コードを設けることも多いので{} で囲む。
あまりにも深いネストは直感的な読み易さを損なう。
Visual Studio では最初の { を入力すると閉じ括弧が強調表示されるようになっている。

鋭いヒトはお気づきの通り、何かが抜けている。略してあるのは端数の転送処理。
今回はたまたま、転送するバイト数が4の倍数と判りきっているので不要だ。しかし、実際には4の倍数以外を転送する、つまり、4で割って余りが出るようなケースも想定すべきなのだ。
余りはゼロから3の間。その場合は残りを1バイトずつ転送すれば良いだろう。最大3回までで済む。
ちなみに、数年前までのIntel 製 CPU では4バイト転送の直後に2バイト単位のアクセスを行うとペナルティ、つまり、遅くなると言われていた。Core 2 Duo , Quad 以降では改善されているようで、2バイトアクセスによる遅延を気にしないでもよさそうだ・・・

このままでもコンパイラが良好なコードを出力してくれる可能性が高い。
あえて、変更を加えてみよう。

SIZE_T j;
j = (__SIZE_DUPLICATE / sizeof(DWORD32) );
while(--j){
    *pdwd++ = *pdws++;
}

インクリメント演算子 (加算演算子) 、デクリメント演算子が読み辛いかも。
演算子を加算後に代入、減算後に代入に置き換えてみると以下のようになる

while(j){
    *pdwd = *pdws;
    pdws += 1;
    pdwd += 1;
    j -= 1;
}

さらに読み辛いヒトは△-=1△ = △-1と置き換えてみればわかるでしょうか・・・

最初の例と比べ for 文だった部分が while 文 へと変更。
最初の例でデータの読み書き部分がポインタと配列の組み合わせだったのに対し、ポインタ値での読み書きとなったため、自前でポインタ値を加算するように変更。
さらに、「特定の数に達したら終了」から「残り数を1つずつ減らし、ゼロになったら終了」へと変更。

for 文 と while 文の違いで速度に影響することはほぼありません。
比較の部分は「ゼロになったら終了」が速いとされてきました。というよりも、遅くならないと言い換えた方が適切かも。詳しくは結果検証のところで・・・

次に 64ビット幅、8バイトずつ転送する案を考えてみる。

DWORD64 *pdwd , *pdws;
pdws = (DWORD64 *)(lpTemp[2]);
pdwd = (DWORD64 *)(lpTemp[3]);
SIZE_T j;
j = (__SIZE_DUPLICATE / sizeof(DWORD64) );
while(--j){
    *pdwd++ = *pdws++;
}

32ビット幅ずつ転送するのに比べ異なる点は 、DWORD32 から DWORD64 への変更。
一度に読み書きできる幅が2倍になった分、読み書き作業の総数も半分となる。繰り返す回数は __SIZE_DUPLICATE として定義した数の1/8。

なお、筆者の環境は64ビット版の Windows 7 である。
32ビット版の OS 上で64ビットコードを転送するコードは返って速度向上は期待できない。なぜならば、32ビット環境では汎用レジスタが4バイト分であり、2回転送するようなコードが生成される可能性が高い。
実際に出力されたアセンブリ言語のファイルで確認するのが手っ取り早い。アセンブリファイルを出力する方法については車輪の再発明 (7)で触れた。

まず、64ビット版 のプラットフォーム向けに出力されたアセンブリファイルのコードは以下の通り。



64ビット幅を持つ1本のレジスタへ読み込み、そのレジスタの値を書き出す。

続いて32ビット版 のプラットフォーム向けに出力されたアセンブリファイル。



32ビット幅を持つ1本のレジスタへ読み込み、そのレジスタの値を書き出す。それを2回行った後ポインタの加算、残りがゼロか比較している。
やはり2回転送するようなコードが生成された・・・

32ビット幅で2度の転送を避けられないならば以下のように翻訳されるならば結果が良くなりそうだ。



32ビット幅を持つ別々の2本のレジスタへ読み込み、ポインタの加算、そのレジスタの値を書き出す。
どのようにC/C++ 言語のコードを組み変えたのか気になるヒトもいるでしょう。64ビット幅のデータ転送が最終目的ならじっくり扱いところですが、あしからず・・・

C/C++ 言語で SSE2 を使わない転送の話はここまで・・・の予定でしたが少々脱線・・・
条件は Intel 製 CPU 、アセンブリ言語、32ビット環境に限られるが、もっと手短かに転送する例として以下のように考えられる。
DWORD dwEcx , dwEsi , dwEdi;
dwEcx = __SIZE_DUPLICATE / sizeof(DWORD);
dwEsi = (DWORD)(lpTemp[2]);
dwEdi = (DWORD)(lpTemp[3]);
__asm{
    mov ecx , dwEcx
    mov esi , dwEsi
    mov edi , dwEdi
    rep movsd
}

上記の例において、__asm{ ~~ }のブロックでインラインアセンブラを使っている。
64ビット環境を前提に C/C++ 言語 で組む場合、インラインアセンブラが廃止されたことによりアセンブリ言語は利用できない。厳密に言えば、Visual Studio でもアセンブリ言語を扱えるのだが、利用の際は厳しい制限がある。
ほかにも、64ビット環境とインラインアセンブラの関係だけでなく最適化が効かない、致命的なエラーを発生させ易いなど気をつけるべき点もあり、「お手軽」とは言えない。

現在、MOVSD は複数の意味があり、ここでは Move data from String to string 「ストリングの移動」を指す。実際には、基のデータを消去しない。よって、移動ではなく複写。
もうひとつは、SSE2 機能を搭載したプロセッサで Move Scalar Double 、「倍精度浮動小数点値 の移動」を指す。
前者の MOVSD は、 ESI の指す番地にあるデータを4バイト読み出し(ロード)、EDI の指す番地に書き込む(ストア)。その後、ESI 、EDI 両レジスタに格納されている値は4加算。
ただし、ステータスレジスタ、Intel 製 CPU では EFLAGS レジスタのディレクションフラグ が 1に設定されているならば、ESI 、EDI 両レジスタともに4減算。

MOVSD の前に REP が付くのは Repeat の略で、「繰り返し」を指す。「ECX の値がゼロになるまで繰り返す」という意味になる。
C/C++ 言語において、for 文や while 文を用いて「残り数がゼロになるまで繰り返す」のと同じようなものだ。

「ストリングの移動」を指す MOVSD 命令は古くから搭載されており、速くないとされてきた。
筆者の個人的な見解かもしれないが「速くない」という表現より、「転送の条件によっては遅れが生じ易い」と言い換えた方が実際の状況に近い。

さてさて、 C/C++ 言語で SSE2 を使った例・・・と続けたいところですが・・・

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

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

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

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

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

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

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

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

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

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