Open4

simdjsonのメモ

onihusubeonihusube

パディングの必要性

パーサーにstd::stringを渡す場合、std::stringの確保しているメモリ領域が固定のパディング分余計になければならない。この領域は、std::stringの有効な文字列長ではなく、std::stringの未使用メモリになければならない。

ondemand::parser parser{};
ondemand::document decomp_json{};

std::string_view input = R(++"{ "test" : 0 }"++);
std::string input_json{input};

// このアサートがない場合、simdjsonのアサートで落ちる。なお原因は分かりづらい
assert((input.length() + SIMDJSON_PADDING) < input_json.capacity());

auto doc = parser.iterate(input_json);

パディングはsimdjson::SIMDJSON_PADDING(これはマクロではない)で得られる。文字列長+SIMDJSON_PADDINGよりも.capacity()の値が大きくなければならない。

対処は.reserve()する。

std::string_view input = R(++"{ "test" : 0 }"++);
std::string input_json{input};

// 閾値以上の値でreserve()する
input_json.reserve(input.length() + SIMDJSON_PADDING + 1);
auto doc = parser.iterate(input_json);

C++20以降、.reserve()は現在のキャパシティよりも小さい値に対して何もしないため、ifでサイズをチェックする手間を省ける。実装によっては前からそうだったかもしれない?

std::string_viewを直接渡す場合

parser::iterate(std::string_view, std::size_t)の第二引数に謎の数字を指定できるがこれは罠である。

ondemand::parser parser{};
ondemand::document decomp_json{};

std::string_view json = R"++({ "test" : 0 })++";

// 第二引数にサイズを渡す、何を渡す?
auto doc = parser.iterate(json, json.length());
// こうする?
auto doc = parser.iterate(json, json.length() + SIMDJSON_PADDING + 1);

このサイズは、第一引数のstd::string_viewの長さに加えて、その領域のお尻にあるパディングサイズ(確保されているメモリ領域)を指定するもの。したがって、上記のように文字列リテラルを参照するstd::string_viewでその長さより大きい値を指定すると未定義動作となる。かといってパディングが不要なわけでもない。

ondemand::parser parser{};
ondemand::document decomp_json{};

std::string input_str = R"++({ "test" : 0 } { "test2" : 1 } { "test3" : 2 } { "test4" : 3 } { "test5" : 4 })++";

// 最初のjson部分だけを参照する
std::string_view json{input_str, 14};

// 第二引数には、文字列長に加えてパディングとして利用可能な領域のサイズを指定する
auto doc = parser.iterate(json, json.length() + SIMDJSON_PADDING + 1);

このように、あらかじめ確保してある領域の一部をsring_viewで参照しているときに、余った領域をパディングとして使用するために使うのが正しい。

そのような領域の余裕がない場合、std::stringにコピーしてreserveするのが正しい行い。

onihusubeonihusube

エラーのタイミング

simdjsonによるパースは全て遅延されている(少なくともondemand::の下にあるものは、他があるのかは知らない)。したがって、parser.iterate()のタイミングでは何もパースしてない。その後要素を参照した時にエラーが起こりうることをしてもエラーにならない。

やっとエラーになるのは、要素を参照し値を取り出そうとしたタイミング。

ondemand::parser parser{};
ondemand::document decomp_json{};

std::string_view input = R"++({ "test" : 0 })++";
std::string input_json{input};

// 十分なパディングの追加を忘れたとする
//input_json.reserve(input.length() + SIMDJSON_PADDING + 1);

auto doc = parser.iterate(input_json);  // エラーにならない
auto r = doc["test"].get_int64();  // エラーにならない
std::int64_t n = r.value();  // ここでようやくエラーになり、例外を投げる
ondemand::parser parser{};
ondemand::document decomp_json{};

std::string_view input = R"++({ "test" : 0 })++";
std::string input_json{input};

input_json.reserve(input.length() + SIMDJSON_PADDING + 1);

auto doc = parser.iterate(input_json); // ok、問題なし
auto r = doc["missing"].get_int64();  // エラーにならない
std::int64_t n = r.value();  // ここでようやくエラーになり、例外を投げる

auto r2 = doc["test"].get_string();  // エラーにならない
std::string_view str = r2.value();  // 例外を投げる

実際に値を取り出そうとするまでエラーが出ないので、エラー発生箇所とその原因は離れたところにありがち。

ネストした要素の引き当てとエラー

とはいえこの仕様は便利ではある。

ondemand::parser parser{};
ondemand::document decomp_json{};

std::string_view input = R"++({ "test" : { "nest1" : { "nest2" : { "value" : 1 } } } })++";
std::string input_json{input};

input_json.reserve(input.length() + SIMDJSON_PADDING + 1);

auto doc = parser.iterate(input_json);

auto r = doc["test"]["nest1"]["nest2"]["value"].get_int64();  // ok
std::int64_t n = r.value();  // ok

// データによってはさらにネストしている値がある場合があるとするとき
auto r2 = doc["test"]["nest1"]["nest2"]["nest3"]["nest4"]["nest5"]["value"].get_int64();  // エラーにならない
std::int64_t n2 = r2.value();  // 例外を投げる

これは無例外インターフェース使用時に特に便利。

ondemand::parser parser{};
ondemand::document decomp_json{};

std::string_view input = R"++({ "test" : { "nest1" : { "nest2" : { "value" : 1 } } } })++";
std::string input_json{input};

input_json.reserve(input.length() + SIMDJSON_PADDING + 1);

auto doc = parser.iterate(input_json);
error_code ec{};

std::int64_t n;
ec = doc["test"]["nest1"]["nest2"]["value"].get_int64().get(n);  // ok

if (ec != error_code::SUCCESS) {
  // データがなかったときの処理
  ...
}

// データによってはさらにネストしている値がある場合があるとするとき
std::int64_t n2;
ec = doc["test"]["nest1"]["nest2"]["nest3"]["nest4"]["nest5"]["value"].get_int64().get(n2);  // ok

if (ec != error_code::SUCCESS) {
  // データがなかったときの処理
  ...
}

途中の要素がなかった場合でも[]による引き当てはそこで例外を投げたり停止したりしないので(実際にはエラーが出たところ以降はパースしていないが)、とにかく特定の要素があれば取り出すとき(どの要素があるかないかに関心がないとき)に便利。

なお、例外の場合はそのメッセージ(.what())にエラー理由が書かれているが、エラーコードの場合はそれがない(simdjson::error_message()という関数はあるがなぜか何も出なかった・・・)ので、エラー理由が知りたい場合は例外を受けた方がいいかもしれない。

onihusubeonihusube

無例外(エラーコード)インターフェース

ondemand::parser parser{};
ondemand::document decomp_json{};

std::string_view input = R"++({ "test" : { "nest1" : { "nest2" : { "value" : 1 } } } })++";
std::string input_json{input};

input_json.reserve(input.length() + SIMDJSON_PADDING + 1);

auto doc = parser.iterate(input_json);
error_code ec{};

std::int64_t n;
ec = doc["test"]["nest1"]["nest2"]["value"].get_int64().get(n);  // ok

if (ec != error_code::SUCCESS) {
  // データがなかったときの処理
  ...
}

このインターフェースの選択は、最後の.get();の呼び出しで決まっている。

途中の失敗しそうな処理は全て、simdjson::simdjson_resultというクラスが返されている。このクラスはその名の通りresult<T, E>な型であり、Tは呼び出す処理によって変わるものの、Eは全ての場合においてsimdjson::error_codeである。

したがって、実は、途中の処理([]とかget_int64()などなど)は全てnoexcept指定されている。

このsimdjson_resultから値を取り出すときに、.value()/.error()などのインターフェースを使用すると、状態に応じて例外によってエラーを報告し、.get()などを用いると引数に値を出力しエラーは戻り値(error_code)で報告する。

[]で要素を引き当てているときはTは変な(simdjson内部の)型だが、get_xxx()を呼び出すことでTが確定し、get()に渡せる参照の型も確定する。

なお、simdjson::simdjson_resultの実装はstd::pairらしく、特に複雑な実装になってはいない。Tはデフォルト構築可能である必要があるが、ユーザーが指定することはないだろう。