12/29 (月)
秋葉原ラジオデパート
iRiver iHP-120 に附属している AC アダプタが断線して充電できなくなったのだが、 AC アダプタの端子が特殊なものらしくて近所の電器屋には売ってない。 ふつう端子は外形が5.5ミリで内径が2.1ミリなのだが、 iHP-120 の AC アダプタは外形が 2.3 ミリ。 秋葉原に出て探し歩いたが結局ぴったりのものはなく、 ラジオデパートで標準の DC ジャックからの変換器を買って、 普通の AC アダプタと組み合わせて使うことに。
ついでに年賀状の印刷用にソフマップでプリンタを購入する。 値段が安いのと重量が軽いので hp Photosmart D5460 を選ぶ。 実家に居た頃に買ったドットインパクトプリンタを除くと、 hp deskjet 948c に続き2台目の自宅プリンタになる。 まぁ、自宅で印刷するような事がないのでほとんど使わんだろうね…
P.S.
よく見ると D5460 って、Windows2000 用のドライバーが附属しないのね。 自宅のデスクトップからは印刷できないナリ。
12/16 (火)
[CPU] Itanium2 の分岐命令は 40 ビット境界を越えた PC 相対分岐が全部分岐ミスになる
今日も今日とて IA-64 命令と格闘の日々なのだが、 仕様書にはっきり記述されていない Itanium2 の変な挙動を見つけて大弱り。
IA-64 命令の PC 相対分岐命令は、符号付き 25 ビットの範囲にジャンプできる BR 命令を使うのだが、 64 ビット全空間に相対分岐可能な BRL 命令という拡張版も用意されている。 昨日の日記にも書いたが IA-64 命令は 41 ビットなので、 BRL 命令はバンドル中の 2 スロットを使い 82 ビットで表現される命令だ。
この BRL 命令は、 コードを動的に生成するようなプログラムでは使い勝手が大変よく頻繁に利用していたのだが、 性能的に悲しい問題があることに気づいた。 どうも BRL命令を使って 40 ビット境界を越える分岐をすると、 フロントエンドに 6 サイクルのパイプラインストールが生じるようだ。 この制限は Intel の出している資料にははっきり書かれている箇所がなく、 唯一 インテル Itanium2 プロセッサ リファレンス・マニュアル ソフトウェアの開発と最適化 の17ページの注に1文だけ触れているだけ。 表を見るだけだと分岐ターゲット予測が外れた場合だけペナルティを受けるように見えるが、 実際は「BRL命令のターゲットが40ビット境界を越える場合は全部分岐ミス」あるいは 「分岐ミスが起こったのと同様のレイテンシ」を喰らってしまうようだ。 PC 相対分岐なのに…
IA-64 は 264 バイトの空間を 261 バイトづつに 8 個に区切って、 個々をリージョン(region)と呼んでいる。 Linux の場合 リージョン毎に用途を分けて使っているのだが、 実行ファイルの実体はリージョン2にマップされ、 スタックやヒープはリージョン3にマップされるのが普通だ。
Region | 用途 |
---|---|
0 | IA-32 エミュレーション |
1 | 共有ライブラリ用 |
2 | テキスト用 |
3 | データ用 |
4 | Huge page |
5-6 | Kernel |
動的生成コードや JIT コードを作る場合、 全ての処理をコンパイルするのではなく、 ランタイムのテキスト領域にヘルパー関数を用意しておいて 必要に応じてコンパイルされたコードから関数呼び出しするような実装が多いと思われる。 この時、 コード用の領域を普通に malloc してリージョン3から確保すると、 ヘルパー関数の呼び出しの度に 6 サイクルのペナルティを受けてしまう。
リージョン2の中にコード領域を malloc するという方法もあるのだが、 性能を追求するとヒープ系のメモリは huge page に配置したい。 そうするとランタイムコードとの距離は必然的に 40 ビット境界を越えてしまうので、 いっそランタイム内のテキスト領域を huge page の方に転写して… そうするとスタックがアンワインドできなくなるからアンワインド情報も一緒に生成して… そうすると DWARF ライブラリが… (以下、無限に続く)。
P.S.
BRL 命令を使った無条件分岐が 6 サイクルのペナルティを受けるかどうかはチェックしていなかった。
追記:2008/12/17
無条件分岐でも40ビット境界を越えると 6 サイクルのペナルティが発生している。
追記:2009/1/2
開発中の JIT コンパイラのヘルパー関数の保持方法を変更して、 コンパイルされたコードとヘルパー関数の距離が 40 ビット境界に納まるようにしてやると、 それだけで 10% ぐらい性能が改善したよ。 トホホ。
12/11 (水)
[Prog] C 言語と C++ 言語で構造体のビットフィールドの扱いが違う
毎度ながら IA-64 命令の変態性が発見のきっかけで…
IA-64 の命令は 16 バイトのバンドルの中に 3 命令含まれているのだが、 各命令は 41 ビットという中途半端な長さだ。 このデータ構造を C 言語で扱おうとすると 以下のようなビットフィールド構造体が欲しいのだが、
typedef struct { uint128_t temp : 5; uint128_t slot0 : 41; uint128_t slot1 : 41; uint128_t slot2 : 41; } ia64_basic_bundle_t;
しかし uint128_t
をあらわせる整数型が通常の処理系にはなく、
IA-64 も含めて大概のアーキテクチャでは uint64_t
を使うことになる。
つまり 64 ビット境界でスロット1の命令を分割することになる。
typedef struct { uint64_t temp : 5; // Slot0 uint64_t slot0 : 41; // Slot1 uint64_t slot1_l : 18; uint64_t slot1_h : 23; // Slot2 uint64_t slot2 : 41; } ia64_basic_bundle_t;
バグの原因を探っていると
スロット0とスロット2からは 41 ビットの IA-64 命令が取り出せるのだが、 スロット1の場合には 64 ビット境界を越えた上位と下位を合成する必要がある。 そこで以下のようなプログラムを書いてみた。
#include <stdio.h>
#include <stdint.h>
#include <inttypes.h>
#include <string.h>
typedef struct {
uint64_t temp : 5;
uint64_t slot0 : 41;
uint64_t slot1_l : 18;
uint64_t slot1_h : 23;
uint64_t slot2 : 41;
} ia64_basic_bundle_t;
int main(int argc, char** argv) {
ia64_basic_bundle_t bundle;
memset(&bundle, 0xFF, sizeof(bundle));
uint64_t slot1 = ((bundle.slot1_h << 18) | (bundle.slot1_l));
uint32_t low = (uint32_t)slot1;
uint32_t high = (uint32_t)(slot1 >> 32);
printf("%08x %08x\n", (uint32_t)high, (uint32_t)low);
return 0;
}
このプログラムでは全てを1で埋めた16バイトからスロット1を取り出して表示するので、000001ffffffffff
と表示されることを期待しているのだが、
どうもうまくいかない。
Linux のいろいろなアーキ/gccの組み合わせで試してみると、
以下のように期待しない結果になる。
しかも最適化依存。
CPU | gcc version | Opt. | Result |
---|---|---|---|
IA-64 | gcc 3.4.6 | -O0 | 00000000ffffffff |
IA-64 | gcc 3.4.6 | -O1 | ffffffffffffffff |
IA-64 | gcc 4.1.0 | -O0 | 00000000ffffffff |
AMD64 | gcc 3.4.6 | -O0 | 00000000ffffffff |
AMD64 | gcc 3.4.6 | -O1 | ffffffffffffffff |
IA-32 | gcc 3.3.6 | -O0 | ffffffffffffffff |
IA-32 | gcc 3.3.6 | -O1 | ffffffffffffffff |
これは gcc のバグという訳ではなく、 もともとこの記法はC++言語では妥当だが C 言語では正しく動作する保証がないのが問題のようだ。
C言語の仕様(C99)では「ビットフィールドの型は、修飾版または非修飾版の _Bool, signed int, unsigned int または他の処理系定義の型でなければならない」とある。 LP64 環境では uint64_t は unsigned long 型なので、これがビットフィールドの型として使えるかどうかは処理系依存ということになる。
一方、C++ 言語の場合はビットフィールドに使える整数型は「汎整数型」であるとされている。 汎整数型は bool型、char型、int型、short型、long型、wchar_t 型の符号付き・なしである。 そのため LP64 環境では unsigned long 型のビットフィールドは許され、期待した動作となる。 ただし LP32 の環境では uint64_t は unsigned long long 型で定義されているが、long long 型は C++ の仕様にはないので処理系依存になる。
C 言語でも ICC などは上記のビットフィールドの演算を期待通り処理してくれるが、移植性を考えると int 型以外のビットフィールドは書けないということを気づかされた orz
12/1 (水)
[CPU] 算術左シフトの覚え書き
右シフトには算術シフトと論理シフトの区別があるのはあたり前なのだが、 左シフトにだって算術シフトと論理シフトの区別はある。 普通は左シフトは論理シフト相当なのだが、 System/370 には論理左シフトとは別に 算術左シフト命令 Shift Left Single (SLA), Shift Left Double (SLDA) が 存在している。
これらの算術シフトでは、 最上位の符号ビットが保存され、 残りのビットだけがシフト操作を受ける。 例えば 0x7FFF,FFFF を左に1ビットシフトすると 0xFFFF,FFFF になる。