アヒルのある日

株式会社AHIRUの社員ブログです。毎週更新!社員が自由に思いついたことを書きます。

バイトオーダー

こんにちは、みにくい社長です。

プログラミングでバイトオーダー(エンディアン)を気にする場面が少なくなってきていると思いますが、 関連して悩んだ部分があったので、今日はその話をしようと思います。

エンディアンとは?

 バイトオーダーというのは複数のバイトを並べる順序のことで、エンディアンともいわれます。 例えば16進数で[1234ABCD]という4バイトのint型の変数があった時、 メモリ上での並びが[12 34 AB CD]となる場合と[CD AB 34 12]となる場合があります。 前者をビッグエンディアン、後者をリトルエンディアンと呼びます。 明らかにビッグエンディアンの方がわかりやすいのですが、 どちらを使用するかはCPUの設計により決まり、コンピュータにとって効率の良い方法が選択されています。 実際、世間のCPUはどうなのかというと、Windowsマシンに多く使われているIntel製のCPUはリトルエンディアンです。 PlayStationやNintendo Switchなどのゲーム機のCPUも、リトルエンディアンの場合とビッグエンディアンの場合があります。

エンディアンの問題点

 今までプログラミングをしていて、エンディアンなんて聞いたことないぞ、と思う方もいるかもしれません。 実際、気にしなくてもプログラミングに支障がないからです。 リトルエンディアンでメモリ上に格納されていたとしても、四則演算は普通にできますし、 Visual Studioなどのデバッガは値を表示する時に10進数の並びを気にしなくて良い値で表示してくれます。 では、エンディアンが問題になるのはどういう時でしょうか。 エンディアンはCPUによって異なるので、他のPCと通信をする場合に問題になります。 ビッグエンディアンのPCがリトルエンディアンのデータを受け取ると、値を間違って解釈してしまいます。 通信時にはこのような問題が起こり得るので、通信プロトコルではビッグエンディアンで送信するというルールになっています。

 また、ファイルを保存する際にどちらのバイトオーダーで保存しているかがわからないと、読み込むPCによって不具合が発生します。 この問題を回避するため、UnicodeではBOMにエンディアン情報を保存できるようになっています。 テキストデータを読み込んだ後に数値に変換する場合は、変換時にバイトオーダーが決まるので問題ありません。 しかし、バイナリで保存されているデータは読み込み時に注意が必要になります。

ゲーム開発における問題

 ゲームを開発する時に、各プラットフォーム(バイトオーダーが異なる)で共通のバイナリデータを使用したいことがあります。 回避策としては3つ考えられます。

1)バイナリデータを使用する本体で作成する

これがシンプルな方法で、プログラムの対応は不要で、問題が発生することはほとんどありません。 懸念点としては、ゲーム機のスペックが低い場合はファイル作成に時間がかかってしまうことです。

2)開発しているPCでリトルエンディアンのデータを作成し、ビッグエンディアンのゲーム機では読み込み時に変換する

この方法では、プログラムを両方のバイトコードに対応する必要があります。 また、ビッグエンディアンのゲーム機では読み込み時に変換負荷がかかってしまいます。

3)開発しているPCでビッグエンディアンとリトルエンディアンの両方のデータを作成する

この方法も、プログラムを両方のバイトコードに対応する必要がありますが、 個別にファイルを用意することで読み込み側の負荷を無くすことができます。

 OROCHIエンジンでは1の手法でバイナリデータを作成していますが、状況に応じて3も行っています。 3の手法で進める際に、次のような問題が発生しました。

構造体のサイズ問題

 バイナリファイルの読み書きでは、変数を構造体にまとめて読み書きすることがあります。 その際、構造体のサイズをsizeofで計算して、memcpyで読み書きするのが一般的です。 しかし、このsizeofが環境によって異なるケースがあります。 まず構造体のメンバ変数の順番ですが、

struct Base1
{
  int* a;    // 8byte
  int b;     // 4byte
  double c;  // 8byte
};

のような構造体のサイズはいくつになるでしょうか? 単純に足すと20byteですが、環境によってはアライメントをそろえる為にpaddingが入るため、

struct Base2
{
  int* a;      // 8byte
  int b;       // 4byte
  char pad[4]; // 4byte
  double c;    // 8byte
  char pad[8]; // 8byte
};

のような32byteになる場合があります。 環境の違いで不具合が発生しないようにするには最初からBase2のように記載しなければならないのですが、 無駄があり、見辛いですよね。 大きい変数から順に定義すると少し改善できます。

struct Base3
{
  int* a;    // 8byte
  double c;  // 8byte
  int b;     // 4byte
  char pad[12]; // 12byte
};

また、structを継承していた場合も注意が必要です。

struct Base4
{
  int a; // 4byte
}
struct Child : public Base
{
  int b; // 4byte
}

Childのサイズは4+4で8byteになりそうですが、こちらも環境によっては16byteになったりすることがあります。 これは、コンパイラによっては継承する際に親の構造体へのアドレスをaとbの変数の間に保持することがある為です。 こればかりは実行してみないとわからない部分でもあり、注意が必要です。

というわけで、バイトオーダーに関する豆知識でした。

では、またねー。