Copyright (c) 2017 Mamoru Kaminaga
https://github.com/mkaminaga
はじめに
仰々しいタイトルであるが, ただの備忘録である.
経験したバグ・不具合に対する原因・対策を整理して記載する.
公開には抵抗を感じるが, もしかすると役に立つかもしれないので投稿する.
この記事は逐次更新する.
なお, 模擬コードは要点を示すために簡略化してあるため, コンパイルが通らないかもしれない.
不具合カテゴリ
パフォーマンス低下
「ゲームループの FPS 突然ガタ落ち問題」
概要
%
演算子と >=
演算子の使い分けの誤りに起因する不具合である.
症状
DirectX と C++ でシューティングゲームを作成していた際, オブジェクト数が増えると突然 FPS が半分近くまで低下する不具合が生じた.
発生個所
ゲームループ中の FPS 調整およびメッセージ処理を行うポーリング関数である.
変数 millisecond
は割り込み的にミリ秒刻みでインクリメントされる.
ゲームループでは 更新・描画, ProcessMessage 関数の順に実行される.
模擬コードを示す.
bool ProcessMessage() {
MSG msg;
while (((millisecond - last_millisecond) % 16) == 0) {
if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
if (msg.message == WM_QUIT) return false;
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
last_millisecond = millisecond;
return true;
}
原因と対策
更新・描画が 16ミリ秒を超えてしまうとこの不具合が発生する.
%
演算子を用いたため, ポーリングを抜けるためには 16 ミリ秒の倍数時間だけ待たなければならないためである.
>=
演算子に変更したところ, だいぶマシになった.
解決後の模擬コードを示す.
bool ProcessMessage() {
MSG msg;
while ((millisecond - last_millisecond) >= 16) {
if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
if (msg.message == WM_QUIT) return false;
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
last_millisecond = millisecond;
return true;
}
不具合カテゴリ
メモリ管理失敗
「スマートポインタがデリータを呼んでくれない問題」
概要
クラスのメンバであるスマートポインタのデリータが呼ばれずメモリリークが発生した.
症状
メモリリークが発生した.
発生個所
メンバに sed::unique_ptr
を持つクラス Hoge
があるとする.
Hoge
はコンストラクタで dammy
にメモリを与え, デストラクタでメモリが開放される.
また, Hoge
はカスタムデリータを持つ std::unique_ptr
に保持されている.
模擬コードをす.
void* Alloc(size_t size) {
return my_allocator.Allocate(size);
}
struct Deleter {
void operator()(void* p) {
my_allocator.DeAllocate(p);
}
};
class Hoge {
public:
Hoge() : dammy(new (Alloc(sizeof(int))) int(0)) {}
private:
std::unique_ptr<int, Deleter> dammy;
};
int main {
std::unique_ptr<Hoge, Deleter> hoge(new (Alloc()) Hoge());
}
原因と対策
デリータがポインタに紐づけられたオブジェクトのデストラクタを呼ばずにメモリを開放していた.
hoge
のデスストラクタが呼ばれなかったため, dammy
はメモリリークする.
デリータで保持するオブジェクトのデストラクタが呼ばれるようにしたところ, この問題は解決した.
問題スマートポインタがデリータを呼ばなかったのではなく, スマートポインタを保持するクラスのデストラクタが呼ばれないことであった.
模擬コードを示す.
void* Alloc(size_t size) {
return my_allocator.Allocate(size);
}
template <class T>
struct Deleter {
void operator()(void* p) {
static_cast<T*>(p)->~T();
my_allocator.DeAllocate(p);
}
};
class Hoge {
public:
Hoge() : dammy(new (Alloc(sizeof(int))) int(0)) {}
private:
std::unique_ptr<int, Deleter> dammy;
};
int main {
std::unique_ptr<Hoge, Deleter<Hoge>> hoge(new (Alloc()) Hoge());
}
template <class T> std::unique_ptr<T, Deleter<T>>
は長ったらしいので
template <class T> using up = std::unique_ptr<T, Deleter<T>>
と up
エイリアスを定義した.`
不具合カテゴリ
勘違い
「fgetwsが文字列を読み込まない」
概要
wcslen
と_countof
を混同していた
症状
fwgets
が配列に1文字たりともデータを読み込まない.
ファイルには明らかに文章が書いてある.
発生個所
以下に模擬コードを示す
wchar_t s[256] = {0};
fgetws(s, wcslen(s), fp);
wchar_t s[256] = {0};
fgetws(s, _countof(s), fp);
原因と対策
原因は, バッファサイズの指定を行うところを, 文字列サイズで行ってしまっていたことである. バッファサイズを取得する方法は_countof
マクロを用いるのが手っ取り早い. しかし, 問題が発生した箇所では_countof
ではなく, 格納されている文字列の長さを返すwcslen
関数を用いていた. なんとなく雰囲気が似ていなくもないが...
対策としては, 関数, マクロのマニュアルをMSDNで読むことがあげられる.
「std::vectorのreserveとresizeの使い分けを誤ったことによる不正メモリアクセス」
概要
std::vector<std::wstring>
でreserve
でcapcacity
を確保した(でも実際は中身がない)要素に対して操作を行おうとして, 不正メモリアクセスが陰的に発生した.
症状
cdbのログに記す.
(1f54.1f20): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
*** WARNING: Unable to verify checksum for main.exe
eax=0000000a ebx=0063f71c ecx=00000002 edx=00540053 esi=0063f71c edi=baadf00d
eip=00ef7949 esp=0063f640 ebp=00000000 iopl=0 nv up ei pl nz na po cy
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00210203
main!TrailingDownVec+0x1f9:
00ef7949 8917 mov dword ptr [edi],edx ds:002b:baadf00d=????????
0:000> k
ChildEBP RetAddr
0063f644 00ee00ff main!TrailingDownVec+0x1f9
(Inline) -------- main!wmemcpy+0xe
(Inline) -------- main!std::char_traits<wchar_t>::copy+0xe
0063f664 00ee6972 main!std::basic_string<wchar_t,std::char_traits<wchar_t>,std::allocator<wchar_t> >::assign+0xaf
(Inline) -------- main!std::basic_string<wchar_t,std::char_traits<wchar_t>,std::allocator<wchar_t> >::_Assign_lv_contents+0xa
(Inline) -------- main!std::basic_string<wchar_t,std::char_traits<wchar_t>,std::allocator<wchar_t> >::operator=+0x12
[...]
発生個所
掲載コードのコメントに記す.
[...]
struct SceneMenuInitInfo {
int num;
int curs;
int fnt_id_normal;
int fnt_id_selected;
SYS_FONTMODE alignment;
};
[...]
struct SceneMenuData {
int num;
int curs;
int fnt_id_normal;
int fnt_id_selected;
SYS_FONTMODE alignment;
std::vector<Point2d> p;
std::vector<std::wstring> txt;
};
[...]
[...]
bool InitSceneMenu(const SceneMenuInitInfo& info, SceneMenuData* scene_menu);
bool CustomizeSceneMenuItem(int curs, const Point2d& p,
const std::wstring& txt,
SceneMenuData* scene_menu);
[...]
[...]
bool InitSceneMenu(const SceneMenuInitInfo& info, SceneMenuData* scene_menu) {
assert(scene_menu);
scene_menu->num = info.num;
scene_menu->curs = info.curs;
scene_menu->fnt_id_normal = info.fnt_id_normal;
scene_menu->fnt_id_selected = info.fnt_id_selected;
scene_menu->alignment = info.alignment;
scene_menu->p.reserve(info.num); // ここも問題!
scene_menu->txt.reserve(info.num); // ここが問題!
return true;
}
bool CustomizeSceneMenuItem(int curs, const Point2d& p,
const std::wstring& txt,
SceneMenuData* scene_menu) {
assert(scene_menu);
if ((curs < 0) || (curs >= scene_menu->num)) return false;
scene_menu->p[curs].set(p);
scene_menu->txt[curs] = txt; // メモリ不正アクセス発生!
return true;
}
[...]
製作中のシューティングゲームのコードの断片である.
メニューを利用するシーンクラスはInitSceneMenu
関数でSceneMenuData
関数を初期化し, CustomizeSceneMenuItem
関数でメニューに実際の項目を書き込む. 問題の個所は, InitSceneMenu
関数内部のstd::vector
のcapacity
をreserve
で変更している個所. capacity
でメモリを確保したところで, std::wstring
の要素が存在しないので動作が不安定になっていたのだろう.
原因と対策
reserve
ではなくresize
を使用するべき. 以前にも同じような問題に直面してqiitaに情報を書き込んだ記事が存在しており, この記事が解決の糸口になった.
参考:C++ vector::reserveの挙動を勘違いしていた件について
不具合カテゴリ
タイトル
概要
症状
発生個所
原因と対策
変更履歴
2017/04/13 最初の公開, パフォーマンス関連の不具合でFPSガタ落ちの件を記載, メモリ管理失敗に新記事を追加
2017/07/24 fgetwsの記事を追加
2017/07/29 std::vectorの記事を追加