AOYAMA Koji's プログラミングブログ - プログラミングを楽しく体験

【技術解説】JSON時代だからこそ知りたい:バイナリーデータで実現する高速化【自動生成☆詰将棋】プログラマー向け実装解説

2025/08/23
【技術解説】JSON時代だからこそ知りたい:バイナリーデータで実現する高速化【自動生成☆詰将棋】

 JSON(辞書・連想配列)は、現代のプログラミングにおいて欠かせない技術でしょう。 筆者も大好きで頻繁に使っています。 しかし自動生成☆詰将棋の思考プログラムのコア部分では、JSONの代わりにバイナリーデータを用いてパフォーマンスを向上させました。 その理由や、やり方などを、詳細に解説します。
 なお自動生成☆詰将棋Webアプリは、この記事などでお楽しみください。

プログラミングブログ記事一覧



[PR]

JSONで広がるプログラミングの世界


 最近プログラミングを始めた方にはJSONのデータ形式は当然に見えるかもしれませんが、 筆者には画期的な技術でした。 古参プログラマーは賛同してくれるでしょう。
 ここでは、まず、JSONのおさらいから進めます。

JSON(じぇいそん)とは


【技術解説】JSON時代だからこそ知りたい:バイナリーデータで実現する高速化【自動生成☆詰将棋】 1手詰▽2一玉▲2三金▲金
 この図の盤面は、以下のJSONコードで表現できます。
{
  // ▽2一玉
  "21" : { 
           "駒種"   : "玉" ,
           "先後"   : "後手" ,
           "成不成" : "不成"
         }
  // ▲2三金
  "23" : { 
           "駒種"   : "金" ,
           "先後"   : "先手" ,
           "成不成" : "不成"
         }
  // ▲金
  "先手持ち駒" : {
                   "金" : 1
                 }
}

 JSONはこのように、キーに値が紐づいているデータ構造です。

辞書および連想配列


 同様のデータ構造は、辞書あるいは連想配列など、プログラミング言語によって異なる呼び名や記述方法をもちますが、本質的にはすべて同じデータ構造です。 すなわち、キーと関連付けられた値を持ちます。
 本記事では JSON の特徴として解説を進めます。

視認性と柔軟性にメリット


 JSONは、キーに値が紐づいているため、視認性が高いです。
 また、柔軟に増減させることができ、保守性の高さも秀逸です。
 適切に使用することで、生産性や保守効率は格段に上がります。

パフォーマンスが要求されるところではデメリットも


 一方でJSONは、シビアにパフォーマンスが要求される処理には弱いです。
 ほとんどのケースでは問題にならないので、生産性や保守効率を重視すべきですが、 ごく一部での処理では、問題になる場合があります。

メモリー確保回数

【技術解説】JSON時代だからこそ知りたい:バイナリーデータで実現する高速化【自動生成☆詰将棋】 JSONデータ構造のメモリー配置イメージ  例えばメモリーの確保は、それなりに負荷のかかる処理で、回数が多いと速度パフォーマンスが悪くなります。
 JSON形式の場合、キーおよび値を格納するためのメモリーの確保が必要です。 Webブラウザーエンジンの実装によって異なるため断言はできませんが、この図の例で数回から十数回程度はメモリー確保が必要でしょう。

データの局所性

 メモリーを複数回確保するということは、メモリー空間内に局所的に、つまりバラバラの位置ににデータを持つことになります。
 ゲーム機等では、使用中のメモリーとメモリーの間に隙間ができ、無駄遣いをしている状態で、容量パフォーマンスが悪くなります。 この問題はメモリーフラグメンテーションと呼ばれます。
 一方、最新のパソコンやスマートフォンでは、仮想化の技術でメモリーフラグメンテーションの問題は解決しています。 しかしデータキャッシュミスという問題は残ります。
 最新のCPUでは、メモリー上のデータはキャッシュして処理されますが、メモリー上でバラバラの位置にあるとそのキャッシュ効率が悪く、 速度パフォーマンスが悪くなります。
 いずれにしても、データの局所性はパフォーマンスが悪化します。

間接参照

 図の矢印のように、JSONの実装では、間接的なデータ参照を採用しています。
 この方式では、キーに対応した「値」に辿りつくまでに、何度か処理を挟むことになり、速度パフォーマンスが悪くなります。
[PR]

バイナリーデータの威力


 自動生成☆詰将棋では、詰手順探索処理でシビアなパフォーマンス要求があります。 そのため、そこで使用する情報を、バイナリーデータ化することにしました。
 実装の説明に入る前に、パフォーマンスに関しての結論から記します。

速度パフォーマンス10倍?!


 筆者は数年前に、詰将棋を解くプログラムを作成したことがありました。 そのときは、休日の趣味プログラミングということもあり、作りやすいJSON形式を採用しました。
 そのプログラムで解くのに数時間かかっていた23手詰問題があり、今回のプログラムで解いてみたところ、なんと10分未満でした。
 当時と比べて、マシンもWebブラウザーエンジンも違いますし、アルゴリズムも改善しているため、直接的な比較は難しいですが、 感覚的には、JSON形式からバイナリー形式にしたことで、数倍から10倍程度速度が向上しています。

Claudeの試算は速度320倍


 生成AIのClaudeに試算してもらったところ、320倍との結果が出ました。 以下が理由とされます。
  • メモリー確保効率:8倍
  • データキャッシュ(分散→連続):10倍
  • 間接参照→直接参照:4倍

 これは、データアクセスに関して、相当に早くなることを表しています。
 詰手順解析処理は、データアクセス以外の処理も多いため、残念ながらここまで高速化はされませんが、 状況次第でバイナリーデータ化は相当に高い威力になることがわかります。

容量パフォーマンスも相当向上


 JSON形式で「▽2一玉」を表現すると、前図の形で64ビットCPU環境ではおそらく合計96バイトのメモリー確保が必要です。
 バイナリー形式のために駒と座標を数値化すると、後述の通り 2バイトで済みますので、相当に容量パフォーマンスが向上します。

バイナリーデータを用いた実装


【技術解説】JSON時代だからこそ知りたい:バイナリーデータで実現する高速化【自動生成☆詰将棋】 バイナリーデータ
 ここからバイナリーデータを使用した、具体的な実装を解説します。

連続領域メモリー確保


 まず初めに、連続領域にメモリーを確保します。 JavaScriptでは以下のように書きます。
let aiPosition = new Uint8Array( iPOSITION_LENGTH );

 Uint8 は unsigned integer 8bits の略、つまり0から255を表す1バイトです。 この記述で、iPOSITION_LENGTH バイトのメモリー領域が確保されます。
 JavaScriptで一般的な配列の Array と異なり、UInt8Array は連続領域に固定サイズのメモリーを確保します。

駒の数値化


【技術解説】JSON時代だからこそ知りたい:バイナリーデータで実現する高速化【自動生成☆詰将棋】 駒の数値化
 バイナリーデータ化するには、駒を数値化する必要があります。 割当はこの図のようにしています。要素ごとに解説します。

駒種

駒種
番号
2進数
0
000
1
001
2
010
3
011
4
100
5
101
6
110
7
111

 ベースの駒種に番号を割り当てます。
 将棋の駒は8種類のため3ビットでちょうど表せます。

成り・不成

成不成
ビット
2進数
不成
0
0000
1
1000

 成りと不成を、1ビットで表現します。

先手後手

先後
ビット
2進数
先手
0
00000
後手
1
10000

 先後も1ビットで表現します。

駒の数値の例

▽玉
▲金
10進数
23
4
2進数
10111
00100
16進数
0x17
0x04

 以上を |(ビット論理和) したものが駒の数値になります。
 例として ▽玉 と ▲金 の数値はこの表になります。 すべて1バイトで表現できます。
 また、成るときは | 0x08 、取って持ち駒にするために不成の先手駒を求めるのは & 0x07 という、簡易なビット演算式で計算できます。 ビット演算は高速かつ省メモリーですので、パフォーマンス向上に寄与する設計です。

座標の数値化


 座標についてはこの記事で解説した形式の、1次元座標を採用しています。 具体的には 0 から 88 の数値です。 1バイトで表現可能で、より厳密には7ビットで表現可能です。

盤上の駒の表現


 盤上の駒は、座標と駒の数値を並べて、駒1つに対して合計2バイトで表現します。
 例えば、「▽2一玉」は、座標が 1 、駒が 23 の2バイトです。 「▲2三金」は 21 と 4 です。
 それを持ち駒を除く盤上に置かれている駒の数だけ並べて、最後は 0xff で終了する形で表現します。
[ 1 , 23 , 21 , 4 , 0xff ]

持ち駒の数値化


 持ち駒は、バイナリーデータ内に、所持個数を入れる形にします。
 先手の持ち駒は aiPosition のインデックス 0 から 7 の位置に、後手の持ち駒は 16 から 23 の位置に設定しています。 インデックスと駒種の番号と一致させています。 そうすることで、プログラミングをしやすくしています。

指し手の情報


 詰手順探索処理において、1手進める度にバイナリーデータをコピーして更新し、次の処理に渡します。 その際、そこまでの指し手の情報も、このバイナリーデータに格納しています。
 形式は、1手につき以下の2要素で2バイトです。これを繰り返し、最後は 0xff で終わります。
[ 初手移動元 , 初手移動先 , 2手目移動元 , 2手目移動先 , ... , 0xff ]

 バイナリーデータ aiPosition のオフセット 24~152 の位置に、指し手の情報が入っています。 最大64手ぶんです。

移動元

数値
対象
0~7
先手持ち駒
16~23
後手持ち駒
153~229
盤上の駒
 移動元の情報は、その駒のaiPosition内のオフセットです。 具体的にはこの表の通りです。
 値が 0~255 に収まっていますので、1バイトで表せます。

移動先

▲1二○不成
▲1二○成
数値
10
138
2進数
00001010
10001010
16進数
0x0a
0x8a
 移動先は盤の1次元座標の数値です。具体的には 0 から 88 です。
 移動と同時に成る場合は、1次元座標が7ビットで収まるため、それに 0x80(2進数 10000000) を |(ビット論理和) した値としています。 例としてはこの表のようになります。

詰み手数およびキャッシュ情報


 詰み手数などの情報も、詰手順探索処理に必要です。
 また、現在の手数や手番、および後手玉の座標など、他の情報に基づいて計算すれば求まるものの、参照頻度が高く、再計算を避けるためにキャッシュとして保持しておきたい情報もあります。
 それらもバイナリーデータに格納しています。具体的には以下です。
  • 手番(先手・後手)
  • 現手数
  • 詰み手数(最大手数)
  • 後手玉1次元座標

 このようにすべてバイナリーデータに含めることで、関数呼び出しの引数が、このバイナリーデータだけで済むようになります。
 詰手順探索処理は、長手数の場合には億を超える再帰的な関数呼び出しが発生するので、パフォーマンスを考慮すると重要です。

盤面ハッシュ


【技術解説】JSON時代だからこそ知りたい:バイナリーデータで実現する高速化【自動生成☆詰将棋】 盤面ハッシュ
 指定マスに駒があるかどうかを判別する処理の高速化のため、盤面ハッシュ情報も保持しています。 具体的には1次元座標の値をインデックスとした配列に、盤上の駒のオフセット値を格納しています。
 なお図の [0] から [89] はこの配列内のオフセットで、自動生成☆詰将棋では +232 した値がバイナリーデータ全体の中でのオフセット値です。

速度パフォーマンス向上

 盤上の指定マスに駒があるかどうかを調べるには、通常は駒の数だけループして検索しますが、 盤面ハッシュがあれば、ループ無しで判定できます。 これにより速度パフォーマンスが向上しています。

容量パフォーマンス悪化

 容量パフォーマンスは悪化しています。 ひとつの局面を表すのに、90バイトが追加で必要です。
 特にこの図の例では、90バイトの中で使用されているのは2バイトのみと、容量効率も悪いです。
 一般的に、容量パフォーマンスと速度パフォーマンスは、トレードオフになることが多く、この例はその典型です。 メモリーを消費することで速度を稼いでいます。

バイナリーデータの問題点


 ここまででバイナリーデータの利点を解説しました。 一方で、問題点もありますので、失敗事例を交えて解説します。

視認性


 明確に問題になるのが、視認性の低さです。 例えば「▽2一玉▲2三金」は、JSONで2次元座標を用いた表現では、デバッガーで表示を見ると例えば以下になります。 これなら説明がなくても理解できそうです。
[{"21":{"駒種":"玉"},{"先後":"後手"},{"成不成":"不成"}},{"23":{"駒種":"金"},{"先後":"先手"},{"成不成":"不成"}}]

 同じ情報が、今回のバイナリーデータでは以下となります。
[1,23,12,4,255]

 スッキリしていてパフォーマンスが良さそうなことはわかりますが、意味がわかりません。
 特に大人数で共同開発する、大規模なプログラムでは、わかりやすさは非常に重要ですので、視認性の低さは、無視できない問題です。

保守性


 保守性の低さも問題になります。
 例として、自動生成☆詰将棋の開発中に、実際に起こしてしまった不具合を取り上げます。
 前述した盤面ハッシュ情報は、開発終盤で追加したものです。 このとき、盤面ハッシュ情報を、盤上の駒の情報の位置より前に挿入しました。
 すると、盤上の駒の情報の、バイナリーデータ内のオフセットが、153~229 から 243~321 に変更になります。
 盤面ハッシュおよび、指し手の移動元情報は、この盤上の駒情報のオフセット値を使用しています。 変更前はすべて255以下でしたが、変更後は駒数が多くなると256以上になります。 バイナリーデータの Uint8Array の各要素は 0~255 の 8ビットで、9ビット目以上は削り落とされますので、意図した数値にならず、不具合が発生しました。
aiPosition[24] = 257;
※aiPosition[24]は1になる
 例えば 257 を代入したつもりが、エラーも出ずに勝手に 1 に変更されます。 この不具合の症状がとてもわかりづらく、解決に時間がかかりました。
 本問題は、結局、盤面ハッシュの格納位置を後ろに変更することで解決しています。
 JSONなら追加位置により問題が発生することはほぼありません。

バイナリーデータを使用すべき箇所


 前述までをふまえて、JSONをバイナリーデータ化すべき箇所について、解説します。

パフォーマンスは高いが生産性は低い


 バイナリーデータを用いると、JSONと比較して、パフォーマンスを向上させられます。 一方で、視認性や保守性、ひいては生産性が低くなります。

パフォーマンスが問題になるのはごく一部


 アプリケーションでパフォーマンスが問題になる場合でも、 パフォーマンスを向上させて意味があるのは、 アプリケーションプログラム全体の中のごく一部であることが一般的です。

基本はJSONで必要なら積極的にバイナリー化


 そのため筆者は、パフォーマンスが問題になっていない場合はもちろん、 問題になっていても、ほとんどのケースではJSONを使用すべきと考えます。
 ただし、バイナリーデータ化することで解決するパフォーマンス問題があるならば、 プログラマーとしては、積極的にバイナリーデータを用いて解決すべきです。

バイナリーデータの演算に慣れる


 バイナリーデータ化するときに、ビット演算を駆使する形にすることで、さらに効率を上げられます。
 その際、2進数や16進数の計算が必要で、JSON世代のプログラマーの皆さんは戸惑うかもしれませんが、慣れていきましょう。
 筆者は、Z80および、PCエンジンやスーパーファミコンで2進数や16進数を扱ってきました。 その結果、例えば2桁の16進数の加減算程度なら暗算できるようになりましたが、そこに至るまでに数年を要しています。
 ぜひ本記事を参考に、バイナリーデータマスターを目指してください。
[PR]

まとめ


 本記事では、バイナリーデータを用いたプログラミングによる、パフォーマンス向上について解説しました。
 現代のプログラミングにおいて JSON(辞書・連想配列) は当然の選択ですし、生産性の高さから積極的な使用を推奨します。
 しかしもし、ボトルネックがデータ設計にあり、バイナリーデータ化することで解決しそうであるならば、 プログラマーの選択として躊躇せず積極的にバイナリーデータ化したいです。
 その際に、本記事が少しでもお役に立てましたら幸いです。

補足

  • プログラムソースコード例はすべて JavaScript です。実際に使用したコードを見やすく切り出して説明を追加しています。
  • メモリーフラグメンテーション問題は著書 [PR][PR]『ドラゴンクエストXを支える技術』(技術評論社)で詳細に解説しているのでぜひそちらもご参照ください。
  • 記事の校正/添削に生成AIの Anthropic Claude を利用しております。
  • 記事内の画像の作成に生成AIの Anthropic Claude を利用しております。
  • 画像内のラスタライズ文字フォントにOpen Font LicenseNoto Sans Japaneseを使用しております。
  • 画像内のラスタライズ文字フォントにOpen Font LicenseNoto Sans Monoを使用しております。

カテゴリー:自動生成☆詰将棋,プログラミング解説
[PR]