simdjson 是什么
simdjson 是一个用来解析JSON数据的 C++ 库,它使用常用的 SIMD 指令和微并行算法来每秒解析千兆字节的 JSON,在Velox, ClickHouse, Doris 中均有使用。
加载和解析 JSON documents
出于性能考虑,simdjson 需要一个末尾有几个字节(simdjson::SIMDJSON_PADDING)的字符串,这些字节可以被读取,但它们的内容不会影响解析。实际上,这意味着 JSON 输入应存储在末尾带有 simdjson::SIMDJSON_PADDING 额外字节的内存区域中。用户不需要对这些额外的字节指定值。
simdjson 库提供了一个类似树的 API,可以通过创建 ondemand::parser 并调用 iterate() 方法来访问它。 iterate() 方法可以快速索引输入字符串,并且可以检测到一些错误(JSON不合法)。以下是针对不同字符串表达方式的示例:
ondemand::parser parser;
auto json = padded_string::load("twitter.json"); // load JSON file 'twitter.json'.
ondemand::document doc = parser.iterate(json); // position a pointer at the beginning of the JSON data
ondemand::parser parser;
auto json = "[1,2,3]"_padded; // The _padded suffix creates a simdjson::padded_string instance
ondemand::document doc = parser.iterate(json); // parse a string
ondemand::parser parser;
char json[3+SIMDJSON_PADDING];
strcpy(json, "[1]");
ondemand::document doc = parser.iterate(json, strlen(json), sizeof(json));
std::string data = "my data";
simdjson::padded_string my_padded_data(data); // copies to a padded buffer
建议不要在应用程序中创建许多 std::string 或许多 std::padding_string 实例来存储 JSON 数据,而是重用相同的缓冲区并限制内存分配。
Documents 就是迭代器
simdjson 解析JSON是按需的,document 并不是被完全解析后的JSON,他是一个用于迭代JSON文本的迭代器。当你在迭代时会触发解析原始的JSON文本,处理括号、逗号之类的分隔符,保证你能够得到结果。这么做对于性能提升非常关键,可以跳过不需要的解析动作
Parser, document 和 JSON的作用域
为了代码合法,需要在解析JSON时保证(1)parser 实例(2)输入的JSON string(3)document实例 都存活。并且要遵守以下两点:
- 一个 parser 实例同时最多有一个 document,因为它持有用于解析而分配的内存。
- 按照设计,每个JSON文档应该只有一个 document 实例。因此,如果必须将文档实例按引用传递给函数以避免复制,避免按值传递。
在 iterate 调用期间,原始 JSON 文本永远不会被修改——只能读取。用完 document 后,可以安全地丢弃数据源(无论是文件还是字符串)。
为了获得最佳性能,Parser实例应该在多个文件上重用:否则会不必要地重新分配内存,这是一个开销较大的过程。
如果需要同时解析多个 document,则应该有多个parser实例。
string_view
simdjson使用std::string_view表示已解析的字符串。string_view避免了复制数据的需要和以空字符结尾的C字符串的陷阱。它使用户更容易将数据复制到他们自己喜欢的类实例中。std::string_view 实例实际上只是指向内存中表示字符串的区域的指针。在 simdjson 中,我们返回 std::string_view 实例,这些实例要么指向您解析的输入字符串内或parser实例内的临时字符串缓冲区,该缓冲区在parser实例被销毁或使用它来解析另一个document之前一直有效。
使用解析后的 JSON
需要牢记以下规则:
- 当访问 document 时,document 实例应保留在作用域内:它是“迭代器”,用于跟踪您在 JSON 文档中的位置。根据设计,每个 JSON 文档有且仅有一个document实例。
- 因为 document 实际上只是一个迭代器,所以在访问同级对象或数组之前,您必须完全消耗当前对象或数组。
- 值只能使用一次,如果打算多次需要它们,应该获取并存储它们。应该只访问对象的键一次。应该只检查一次数组的值
以下具体说明指示如何在启用异常时使用 JSON:
-
校验使用的东西。调用iterate时,document 会被快速索引。如果它不是有效的Unicode(UTF-8)字符串,或者如果存在未关闭的字符串,则可能会立即报告错误。但是它没有完全验证整个JSON。仅完全验证被使用的值和相关结构。这意味着在遍历document的每一步,都可能遇到错误。可以处理带有异常或错误代码的错误。
-
提取值。可以将 JSON元素转换为 native type:double(element)。这适用于
std::string_view
, double, uint64_t, int64_t, bool, ondemand::object and ondemand::array. 也有显示方法get_string()
,get_double()
,get_uint64()
,get_int64()
,get_bool()
,get_object()
andget_array()
. 当调用显式或隐式的转换方法后,可能会抛出无法转换的异常。 -
字段访问。要获取对象中“foo”字段的值,请使用
object["foo"]
。这将扫描对象,查找具有匹配字符串的字段,并进行逐个字符的比较。如果对象中没有这样的键,可能会生成错误 simdjson::NO_SUCH_FIELD,可能会抛出异常。为了获得最佳性能,应该尝试按照键在文档中出现的顺序来查询键。 -
输出成string。给定 JSON 文档中的文档、值、数组或对象,您可以输出适合再次解析为 JSON 内容的 JSON 字符串版本:
simdjson::to_json_string(element)
.对 to_json_string 的调用会完全consume该元素:如果将其应用于 document,则 JSON 指针将前进到document的末尾。to_json_string
函数不应与获取字符串实例的值相混淆,该字符串实例是使用指向parser实例内的内部字符串缓冲区的轻量级 std::string_view 实例进行转义和表示的。为了说明这一点,以下两个代码段中的第一个将打印带有引号的未转义字符串“test”,而第二个代码段将打印字符串的转义内容(不带引号):test。// serialize a JSON to an escaped std::string instance so that it can be parsed again as JSON auto silly_json = R"( { "test": "result" } )"_padded; ondemand::document doc = parser.iterate(silly_json); std::cout << simdjson::to_json_string(doc["test"]) << std::endl; // Requires simdjson 1.0 or better
// retrieves an unescaped string value as a string_view instance auto silly_json = R"( { "test": "result" } )"_padded; ondemand::document doc = parser.iterate(silly_json); std::cout << std::string_view(doc["test"]) << std::endl;
-
还有一些其他的,详见官方文档
错误处理
默认情况下,simdjson 库会在出现错误时引发异常 (simdjson_error)。如果在代码中省略了 try-catch,未捕获的异常将停止程序,有两种方法处理错误,一是单个 try/catch 子句进行错误处理,二是使用无异常方法。
-
try/catch 子句进行错误处理
bool simple_error_example_except() { TEST_START(); ondemand::parser parser; auto json = R"({"bad number":3.14.1 })"_padded; try { ondemand::document doc = parser.iterate(json); double x = doc["bad number"].get_double(); std::cout << "Got " << x << std::endl; return true; } catch(simdjson_error& e) { // e.error() == NUMBER_ERROR std::cout << e.error() << std::endl; return false; } }
-
使用异常和单个 try/catch 子句进行错误处理使代码变得简单,但它使您无法控制错误。为了更轻松地调试或更强大的错误处理,您可能需要考虑使用无异常方法。整个 simdjson API 都可以使用无异常方法。所有可能失败的 simdjson API 都会返回 simdjson_result,它是一个 <value, error_code> 对。可以使用 .get() 检索值而不生成异常,如下所示:
ondemand::document doc; auto error = parser.iterate(json).get(doc); if (error) { cerr << error << endl; exit(1); }
如果没有错误,则返回错误代码 simdjson::SUCCESS:它的计算结果为布尔值 false。有几个错误代码来指示错误,它们的计算结果为布尔值为 true。当使用无异常的API时,有责任在使用结果之前检查错误:如果有错误,结果值将无效,使用它将导致未定义的行为。
用一个例子来说明,我们尝试访问一个无效的值(3.14.1)。如果我们想继续而不抛出和捕获异常,我们可以这样做:
bool simple_error_example() { ondemand::parser parser; auto json = R"({"bad number":3.14.1 })"_padded; ondemand::document doc; if (parser.iterate(json).get(doc) != SUCCESS) { return false; } double x; auto error = doc["bad number"].get_double().get(x); // returns "simdjson::NUMBER_ERROR" if (error != SUCCESS) { std::cout << error << std::endl; return false; } std::cout << "Got " << x << std::endl; return true; }
参考
https://github.com/simdjson/simdjson/blob/master/doc/basics.md
https://simdjson.org/