LoginSignup
3
4

More than 5 years have passed since last update.

バグとの壮絶な戦いの記録(備忘録)

Last updated at Posted at 2017-04-12

Copyright (c) 2017 Mamoru Kaminaga
https://github.com/mkaminaga

はじめに

仰々しいタイトルであるが, ただの備忘録である.
経験したバグ・不具合に対する原因・対策を整理して記載する.
公開には抵抗を感じるが, もしかすると役に立つかもしれないので投稿する.
この記事は逐次更新する.

なお, 模擬コードは要点を示すために簡略化してあるため, コンパイルが通らないかもしれない.

不具合カテゴリ

パフォーマンス低下

「ゲームループの FPS 突然ガタ落ち問題」

概要

% 演算子と >= 演算子の使い分けの誤りに起因する不具合である.

症状

DirectX と C++ でシューティングゲームを作成していた際, オブジェクト数が増えると突然 FPS が半分近くまで低下する不具合が生じた.

発生個所

ゲームループ中の FPS 調整およびメッセージ処理を行うポーリング関数である.
変数 millisecond は割り込み的にミリ秒刻みでインクリメントされる.
ゲームループでは 更新・描画, ProcessMessage 関数の順に実行される.
模擬コードを示す.

before
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 ミリ秒の倍数時間だけ待たなければならないためである.
>= 演算子に変更したところ, だいぶマシになった.
解決後の模擬コードを示す.

after
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 に保持されている.
模擬コードをす.

before
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 はメモリリークする.
デリータで保持するオブジェクトのデストラクタが呼ばれるようにしたところ, この問題は解決した.
問題スマートポインタがデリータを呼ばなかったのではなく, スマートポインタを保持するクラスのデストラクタが呼ばれないことであった.
模擬コードを示す.

before
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文字たりともデータを読み込まない.
ファイルには明らかに文章が書いてある.

発生個所

以下に模擬コードを示す

before
wchar_t s[256] = {0};
fgetws(s, wcslen(s), fp);
after
wchar_t s[256] = {0};
fgetws(s, _countof(s), fp);

原因と対策

原因は, バッファサイズの指定を行うところを, 文字列サイズで行ってしまっていたことである. バッファサイズを取得する方法は_countofマクロを用いるのが手っ取り早い. しかし, 問題が発生した箇所では_countofではなく, 格納されている文字列の長さを返すwcslen関数を用いていた. なんとなく雰囲気が似ていなくもないが...
対策としては, 関数, マクロのマニュアルをMSDNで読むことがあげられる.

「std::vectorのreserveとresizeの使い分けを誤ったことによる不正メモリアクセス」

概要

std::vector<std::wstring>reservecapcacityを確保した(でも実際は中身がない)要素に対して操作を行おうとして, 不正メモリアクセスが陰的に発生した.

症状

cdbのログに記す.

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
[...]

発生個所

掲載コードのコメントに記す.

scene_menu_definition.h
[...]
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;
};
[...]
scene_menu.h
[...]
bool InitSceneMenu(const SceneMenuInitInfo& info, SceneMenuData* scene_menu);
bool CustomizeSceneMenuItem(int curs, const Point2d& p,
                            const std::wstring& txt,
                            SceneMenuData* scene_menu);
[...]
scene_menu.cc
[...]
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::vectorcapacityreserveで変更している個所. capacityでメモリを確保したところで, std::wstringの要素が存在しないので動作が不安定になっていたのだろう.

原因と対策

reserveではなくresizeを使用するべき. 以前にも同じような問題に直面してqiitaに情報を書き込んだ記事が存在しており, この記事が解決の糸口になった.
参考:C++ vector::reserveの挙動を勘違いしていた件について

不具合カテゴリ

タイトル

概要

症状

発生個所

原因と対策

変更履歴

2017/04/13 最初の公開, パフォーマンス関連の不具合でFPSガタ落ちの件を記載, メモリ管理失敗に新記事を追加
2017/07/24 fgetwsの記事を追加
2017/07/29 std::vectorの記事を追加

3
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
4