無為空間
むいむい(´ω`*)
Entries
ぱっと見どこかに問題ありそうなのだが、その実なんとか動きそうで、でもやっぱり欠陥を含んでいるという、なかなか挑発的なコードに出会った。
開発環境はVC8。構造体アライメントは8バイト。バイトオーダーはリトルエンディアン。
unsigned char _buffer[27]; ///< データバッファ
まず27バイトのデータバッファがある。
/// データバッファ範囲 struct BufferRange { size_t offset; ///< データバッファオフセット size_t size; ///< データサイズ }; /// データバッファ格納データ範囲一覧 const BufferRange BUFFER_RANGES[] = { { 0, 10 }, // データHoge用バッファ範囲(0?9) { 10, 16 }, // データFuga用バッファ範囲(10?25) { 26, 1 }, // データPiyo用バッファ範囲(26) }; /// データバッファ格納データインデックス一覧 enum BufferIndex { BUFFER_INDEX_HOGE = 0, ///< データHogeインデックス BUFFER_INDEX_FUGA, ///< データFugaインデックス BUFFER_INDEX_PIYO, ///< データPiyoインデックス }; void Store(BufferIndex index, void* data) { const BufferRange& range = BUFFER_RANGES[index]; ::memcpy(_buffer + range.offset, data, range.size); }
データバッファには
- データHoge(10バイト)
- データFuga(16バイト)
- データPiyo(1バイト)
の3つのデータを格納することになっている。
// データHoge構造体 struct Hoge { long val1; // 4バイト long val2; // 4バイト short val3; // 2バイト }; void StoreHoge(const Hoge& hoge) { Store(BUFFER_INDEX_HOGE, &hoge); }
StoreHoge関数は構造体Hogeの内容をデータHoge用バッファにコピーする。
構造体Hogeのサイズは4バイト・4バイト・2バイトに境界調整(JIS X 3014:2003 §3.9/5)用2バイトを含めて12バイト。
それに対しデータHoge用バッファのサイズは10バイトで、コピーするバイト数も10バイト。
一見、バッファにコピーするバイト数が足りていないように見える。
が、コピーされない2バイトは境界調整用の空き領域のため、最低限必要な情報量はコピーされる。
void LoadHoge(Hoge& hoge) { const BufferRange& range = BUFFER_RANGES[BUFFER_INDEX_HOGE]; ::memcpy(&hoge, _buffer + range.offset, sizeof(hoge)); }
LoadHoge関数はデータHoge用バッファから構造体Hogeにデータをコピーする。
この関数はStoreとは逆に2バイト余計に(データHoge用バッファから10バイト、データFuga用バッファから2バイト)コピーしているが、余計な2バイトは構造体末尾の境界調整用の空き領域にコピーされるため問題は起こらない。
// データFuga構造体 struct Fuga { long val1; // 4バイト long val2; // 4バイト long val3; // 4バイト }; void StoreFuga(const Fuga& fuga) { Store(BUFFER_INDEX_FUGA, &fuga); }
構造体Fugaのサイズ(12バイト)がデータFuga用バッファのサイズ(16バイト)より小さいという、Hogeの逆パターン。
一見、書き込み先の方が大きいためバッファオーバーフローの危険はないように見える。
が、逆に書き込み元が小さいためバッファオーバーリードが起こる。
今回書き起こした例は4バイトしかオーバーしないため問題が顕在化することはないかもしれない。
実際のコードでは30バイトほどオーバーしていたため、まれに読み込み可能領域外まで読みに行ってAccess Violationを起こすことがあった。
void LoadFuga(Fuga& fuga) { const BufferRange& range = BUFFER_RANGES[BUFFER_INDEX_FUGA]; ::memcpy(&fuga, _buffer + range.offset, sizeof(fuga)); }
Loadは問題ない。
// データPiyo列挙体(最大情報量1バイトの符号無し整数) enum Piyo { PIYO1, PIYO2, PIYO3, }; void StorePiyo(Piyo piyo) { Store(BUFFER_INDEX_PIYO, &piyo); }
列挙体Piyoのサイズは処理系依存(JIS X 3014:2003 §7.2/5)だが、今回の環境ではintと同じ4バイト。
それに対してデータPiyo用バッファのサイズは1バイトで、コピーするバイト数も1バイト。
一見、バッファにコピーするバイト数が足りていないように見える。
が、バイトオーダーがリトルエンディアンなので、「コピーする先頭1バイト=下位1バイト=データPiyoの全情報」となり、先頭1バイトだけで必要十分な量のコピーが行われることになる。
void LoadPiyo(Piyo& piyo) { const BufferRange& range = BUFFER_RANGES[BUFFER_INDEX_PIYO]; piyo = static_cast<Piyo>(_buffer[range.offset]); }
データバッファの1バイトを列挙型Piyoにstatic_castする。
列挙型の列挙値の範囲にある場合、値は変換によっても変化しないが、そうでない場合の結果の列挙値は規定されていない(JIS X 3014:2003 §7.2/9)。
データバッファの型が符号付き型でかつ列挙型Piyoに128以上の値が与えられた列挙子があったら(符号付き型では負数になるため)問題になるところだが、幸いデータバッファの型は符号無し型なので、ここは特に問題にはならない。
ほかが酷いためつい警戒してしまうが、これはそんな目くじらを立てるようなコードでもないか。
……とまあ、こんな感じのコードが延々と続く。
環境依存であることを理解した上で敢えてこう書いているのか、はたまた「動くようにがんばったらこうなった」のか。
いずれにしろレビューではじくべきコードである。
// コンパイルオプション /DUNICODE 付き HANDLE event = ::CreateEvent(NULL, FALSE, FALSE, (LPCTSTR)"HOGE");
最近というか、今年に入ってから書かれたコードでこの「マルチバイト文字列からUNICODE文字列ポインタへの強制キャスト」に2回も出会っている。
しかもレビューをくぐり抜け、結合段階まで行っている。
Cスタイルキャストの愛用者にはこのキャストが問題ないように見えるのだろうか?
いや、逆か。問題があることを理解しないからCスタイルキャストを使い続けているのか。
Hoge* _hoge; public: Fuga() { _hoge = new Hoge; } ~Fuga() { delete (Hoge*)_hoge; // 意味のないキャスト }
delete時にキャストする必要があるのは
「仮想デストラクタを持たない基底クラスのポインタから適切なポインタにダウンキャストしなければならない」
か
「void*ポインタ(あるいはそれに相当する整数値)から適切なポインタにキャストしなければならない」
という危険な設計の場合のみ。
それ以外の無意味なキャストは可読性と保守性を下げ、無駄にバグを埋め込める場所を増やす役割しか持たないことを理解していれば、こんなキャストを書く気にはならないはずなのだ。
fgetsかfgetwsか
タグ: VS2005VC8でShift JIS(マルチバイト文字列)のファイルをUnicode(ワイド文字列)に変換しながら読み込む方法として、
- fgetws()でUnicodeに変換しながら読む
- fgets()で読んでMultiByteToWideChar()でUnicodeに変換する
のどちらが速いか。
試しに、手元にあった29207行946459バイトのMS-IME用辞書ファイル(Shift JISのテキストファイル)の読み込み速度を、Pentium 4 3.6GHzのWindows XP PCで比較してみた。
#include <windows.h> #include <stdio.h> #include <locale.h> #include <iostream> void test() { ::setlocale(LC_ALL, "japanese"); // MS-IME用辞書ファイルを開く FILE* fp = ::fopen("msime.txt", "rt"); if (! fp) { std::cerr << "ひらけない(´・ω・`)" << std::endl; return; } DWORD start; int line; // 交互に5回試す for (int i = 0; i < 5; i++) { wchar_t wbuff[256] = { 0 }; const size_t wbufflen = sizeof(wbuff) / sizeof(*wbuff); // fgetws()でUnicodeに変換しながら読む ::fseek(fp, 0, SEEK_SET); start = ::GetTickCount(); for (line = 0; ::fgetws(wbuff, wbufflen, fp); line++) { // 1行254文字以下(改行コード混み)を想定 if (wbuff[wbufflen - 2] != 0) { std::cerr << "ながい('・ω・`)" << std::endl; break; } } std::cout << "fgetws " << line << "lines " << ::GetTickCount() - start << "ms" << std::endl; // fgets()で読んでMultiByteToWideChar()でUnicodeに変換する ::fseek(fp, 0, SEEK_SET); start = ::GetTickCount(); char buff[wbufflen * 2] = { 0 }; const size_t bufflen = sizeof(buff) / sizeof(*buff); for (line = 0; ::fgets(buff, bufflen, fp); line++) { // 1行510バイト以下(改行コード混み)を想定 if (buff[bufflen - 2] != 0) { std::cerr << "ながい('・ω・`)" << std::endl; break; } // CP932で最大510バイトをUnicode最大255文字にマップ if (::MultiByteToWideChar(932, MB_PRECOMPOSED, buff, -1, wbuff, wbufflen) == 0) { std::cerr << "おかしい('・ω・`)" << std::endl; break; } } std::cout << "fgets " << line << "lines " << ::GetTickCount() - start << "ms" << std::endl; } ::fclose(fp); } void main() { test(); }
fgetws 29207lines 125ms fgets 29207lines 15ms fgetws 29207lines 94ms fgets 29207lines 16ms fgetws 29207lines 93ms fgets 29207lines 16ms fgetws 29207lines 94ms fgets 29207lines 15ms fgetws 29207lines 78ms fgets 29207lines 16ms
差はおよそ6倍。
fgetws()は「マルチバイト文字列用のバッファを用意する必要がない」とか「マルチバイト文字の泣き別れを気にしなくて済む」という点で楽をできるが、その分コストは高めである。
Visual Studio 2005のARMv4I向けコンパイラが構造体アライメントをどのように扱うか実験。
#pragma pack(push, 1) // 1バイトアライメントの構造体 // sizeof(Hoge) == 10 struct Hoge { short a; // 2バイト int b[2]; // 4バイト×2 }; #pragma pack(pop) void zerohoge(Hoge& hoge); void zeroint(int& n);
// 非インライン関数 void zerohoge(Hoge& hoge) { // 3. 1バイトアライメントの構造体のデータメンバに値を代入する hoge.a = 0; hoge.b[0] = 0; hoge.b[1] = 0; } // 非インライン関数 void zeroint(int& n) { // 4. 組み込み型に値を代入する n = 0; }
void test() { Hoge hoges[2]; // 1. 1バイトアライメント構造体の4バイトのデータメンバが4バイト境界に整列されている hoges[1].a = 0; hoges[1].b[0] = 0; hoges[1].b[1] = 0; // 2. 1バイトアライメント構造体の4バイトのデータメンバが4バイト境界に整列されていない hoges[0].a = 0; hoges[0].b[0] = 0; hoges[0].b[1] = 0; // 3.a. 4バイト境界から始まる1バイトアライメントデータを渡す zerohoge(hoges[0]); // 3.b. 4バイト境界以外から始まる1バイトアライメントデータを渡す zerohoge(hoges[1]); // 4.a. 4バイト境界以外から始まる4バイトアライメントデータを渡す(!) zeroint(hoges[0].b[0]); Hoge* hoge = new Hoge; // 5.a 1バイトアライメント構造体のデータメンバにmemsetする ::memset(hoge->b, 0, sizeof(hoge->b)); // memsetが最適化で消滅するのを防止するために非インライン関数呼び出しを行う zerohoge(*hoge); // 5.b 1バイトアライメント構造体のデータメンバではないとコンパイラに誤認させてmemsetする(!) ::memset(&hoge->b[0], 0, sizeof(hoge->b)); // memset(ry zerohoge(*hoge); delete hoge; }
このコードを/O2(実行速度最適化)でビルドし、要所要所「逆アセンブルを表示」しながら実行してみる。
// 1. 1バイトアライメント構造体の4バイトのデータメンバが4バイト境界に整列されている hoges[1].a = 0; hoges[1].b[0] = 0; hoges[1].b[1] = 0; 00011008 mov r3, #0 0001100C mov r2, #0 00011010 mov r1, #0 00011014 add r0, sp, #0 00011018 strh r3, [sp, #0xA] 0001101C str r2, [sp, #0xC] 00011020 str r1, [sp, #0x10]
Hoge
構造体のアライメントは1バイトだが、hoges[1]
のデータメンバb
はスタックメモリ上において4バイト境界に整列されることが確実なため、strh
(2バイトストア)命令1つとstr
(4バイトストア)命令2つという最少の命令数で全てのデータメンバに0を設定している。
// 2. 1バイトアライメント構造体の4バイトのデータメンバが4バイト境界に整列されていない hoges[0].a = 0; hoges[0].b[0] = 0; hoges[0].b[1] = 0; 00011028 mov lr, #0 0001102C mov r4, #0 00011030 mov r3, lr, lsr #16 00011034 mov r2, r4, lsr #16 00011038 mov r1, #0 0001103C add r0, sp, #0xA 00011040 strh r3, [sp, #4] 00011044 strh r2, [sp, #8] 00011048 strh r1, hoges 0001104C strh lr, [sp, #2] 00011050 strh r4, [sp, #6]
hoges[0]
のデータメンバは、4バイト境界には整列されていないものの2バイト境界には揃えられているので、strh
命令5つで0を設定している。
// 3. 1バイトアライメントの構造体のデータメンバに値を代入する hoge.a = 0; 000110B8 mov r3, #0 000110BC strb r3, hoge 000110C0 strb r3, [r0, #1] hoge.b[0] = 0; 000110C4 strb r3, [r0, #2] 000110C8 strb r3, [r0, #3] 000110CC strb r3, [r0, #4] 000110D0 strb r3, [r0, #5] hoge.b[1] = 0; 000110D4 strb r3, [r0, #6] 000110D8 strb r3, [r0, #7] 000110DC strb r3, [r0, #8] 000110E0 strb r3, [r0, #9]
zerohoge()
の引数hoge
は、コンパイル段階では偶数バイト奇数バイトどちらから始まるかも不明なので、どちらでも問題なく動作するようstrb
(1バイトストア)命令10個で0を設定している。
// 4. 組み込み型に値を代入する n = 0; 000110E8 mov r3, #0 000110EC str r3, n
zeroint()
の引数nは4バイトアライメントの組み込み型の参照型(=実引数が4バイト境界に整列されていることを呼び出し側が保証しなければならない)なので、str
命令1つで0を設定する。
よって以下のように「4バイト境界に整列されていないデータメンバ」を渡すと、
// 4.a. 4バイト境界以外から始まる4バイトアライメントデータを渡す(!) zeroint(hoges[0].b[0]); 00011058 add r0, sp, #2 0001105C bl |zeroint ( 110e8h )|
ARMv4Iのような非境界整列アクセスに対応していない環境では問題が起こる。
Data Abort: Thread=8eff10a8 Proc=8c369c10 'test.exe' AKY=00002001 PC=000110ec(test.exe+0x000010ec) RA=00011060(test.exe+0x00001060) BVA=1c02fe46 FSR=00000003 Unhandled exception at 0x000110ec in test.exe: 0x80000002: Datatype misalignment.
私の実行環境はここで落ちるので、とりあえずzeroint()
の中身をコメントアウトして先に進める。
Hoge* hoge = new Hoge; 00011060 mov r0, #0xA 00011064 bl 000110FC 00011068 mov r4, r0 // 5.a 1バイトアライメント構造体のデータメンバにmemsetする ::memset(hoge->b, 0, sizeof(hoge->b)); 0001106C add r0, r4, #2 00011070 mov r2, #8 00011074 mov r1, #0 00011078 bl 00011114
ここは特に何の工夫もなく、普通にmemset()
を呼んでいる。
// 5.b 1バイトアライメント構造体のデータメンバではないとコンパイラに誤認させてmemsetする(!) ::memset(&hoge->b[0], 0, sizeof(hoge->b)); 00011084 mov r3, #0 00011088 str r3, [r4, #2] 00011090 str r3, [r4, #6]
4バイト境界に整列されているはずの8バイトを0初期化するのにmemset()
を呼ぶのも煩わしいと、str
命令2個で片付けられている。
当然これは非境界整列アクセスになってしまうため、ARMv4Iでは問題になる。
Data Abort: Thread=8efe8658 Proc=8c369d00 'test.exe' AKY=00004001 PC=00011088(test.exe+0x00001088) RA=00011084(test.exe+0x00001084) BVA=1e030062 FSR=00000003 Unhandled exception at 0x00011088 in test.exe: 0x80000002: Datatype misalignment.
「1バイトアライメントの領域を指すポインタ(参照)」から「4バイトアライメントの領域を指すポインタ(参照)」への変換は、本来reinterpret_cast並に気を配らなければならない行為である。
しかし構造体アライメントはC言語の仕様の範疇ではなく、その変換を明示的に行う方法(unpack_castとでも言おうか)も用意されていない。
コンパイラはコードを信じて「暗黙のunpack」を行うしかなく、プログラマはコンパイラが「誤解」しない書き方を把握しなければならない。
C言語やC++でポインタを扱う際には、それ自身が持つ「型」と「値(アドレス)」のほかに、以下の4つの情報を管理しなければならない。
- 正当性
- 終端
- 複製処理
- 解放処理
正当性
ポインタを逆参照しても良いか否か。
以下の3つの状態が存在する。
- 値がNULL(INVALID_HANDLE等の特殊な無効値含む)で、逆参照してはならない
int* p = NULL;
- 値が非NULLで、逆参照しても良い
p = new int(0);
- 値が非NULLで、逆参照してはならない
delete p;
正しく管理しないと、セグメンテーション違反(access violation)の要因となる。
「ポインタはNULL初期化する」「deleteする時点でそのオブジェクトを参照しているポインタは1つだけになるようにする」「deleteしたらすぐにNULLを代入する」などといった行為を徹底し、できるだけ3番の状態を避けるようにすることで、「NULLなら逆参照してはならない」「非NULLなら逆参照しても良い」という分かりやすい状態にできる。
ただし「終端情報としてのポインタ(次項2番)」は例外で、こればかりは「逆参照してはならない非NULLポインタ」という状態を認めなければならない。
終端
ポインタが配列を指している場合に、その終端を示す情報。
以下の3種に大別される。
- 要素数
int sum(const int* p, size_t size) { int result = 0; for (size_t i = 0; i < size; i++) { result += p[i]; } return result; }
- ポインタ(反復子)
int sum(const int* first, const int* last) { int result = 0; for (; first != last; ++first) { result += *first; } return result; }
- 値(0値終端)
size_t count_lower_case(const char* string) { size_t num = 0; char ch; for (const char* p = string; ch = *p; ++p) { if (ch >= 'a' && ch <= 'z') { num++; } } return num; }
正しく管理しないと、バッファオーバーランの要因となる。
複製処理
ポインタをコピー(代入)するときにしなければならないこと。
「何もしない」「newしてディープコピー」「そもそもコピーしてはならない」「参照数を増やす」等。
正しく管理しないとメモリリークや二重解放、正当性情報の破壊(引いてはセグメンテーション違反)を引き起こす。
「何もしない」以外の、コーディングが必要な箇所が多ければ多いほど、見通しが悪くなってバグを含みやすくなる。
可能ならばスマートポインタの使用を徹底し、生ポインタの複製処理については「何もしない」で統一したいところ。
解放処理
ポインタを無効にする(NULLを代入するか、ポインタ変数の生存期間が終了する)前にしなければならないこと。
「何もしない」「free」「delete」「fclose」「参照数を減らす」等。
正しく管理しないとメモリリークや二重解放、正当性情報の破壊を引き起こす。
『C++において基本的かつ重要と思う機能?』でも書いたが、これはスマートポインタ(のデストラクタ)に任せた方が良い。