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实例 都存活。并且要遵守以下两点:

  1. 一个 parser 实例同时最多有一个 document,因为它持有用于解析而分配的内存。
  2. 按照设计,每个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

需要牢记以下规则:

  1. 当访问 document 时,document 实例应保留在作用域内:它是“迭代器”,用于跟踪您在 JSON 文档中的位置。根据设计,每个 JSON 文档有且仅有一个document实例。
  2. 因为 document 实际上只是一个迭代器,所以在访问同级对象或数组之前,您必须完全消耗当前对象或数组。
  3. 值只能使用一次,如果打算多次需要它们,应该获取并存储它们。应该只访问对象的键一次。应该只检查一次数组的值

以下具体说明指示如何在启用异常时使用 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() and get_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 子句进行错误处理,二是使用无异常方法。

  1. 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;
        }
      }
    
  2. 使用异常和单个 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/

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部