neuron-explorer
Qwen3.5‑4B 전용 조향형 뉴런 탐색기, 모든 MLP 뉴런을 밀어 결과를 보관·탐색
Oyl은 최적화된 YAML 라이브러리로, C11 기반 빠른 YAML 1.2 파서·이머터이며 SIMD 스캔과 견고한 이벤트 파서를 지원
Oyl, the optimized YAML library: a fast, minimal YAML 1.2 parser and emitter in C11 with SIMD scanning, event parser, and robust emitter.
개발 도구C · ShellGitHub @openbohemians온라인운영자 미확인
써 보셨어요? 한 번만 눌러 주세요. 로그인은 필요 없어요.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기Fast, minimal YAML 1.2 parser and emitter in C11 with SIMD scanning
저장소 README에서 가져옴
README 전체를 불러오는 중입니다.
README 전문 보기 ↗The optimized YAML library: a YAML 1.2 parser and emitter written in C11. Fast, minimal, zero-copy. (Formerly yam; see the changelog for what the rename changed.)
Website · API reference · Benchmarks · Changelog · Crystal binding (oyl.cr) · Rust binding (oyl.rs) · YAML to JSON for Go (oyl.go)
Features a SIMD-accelerated scanner (SSE4.2 with scalar fallback), an event-based parser, an emitter with block/flow/minimal output styles, merge key expansion, alias resolution, file input, structured error messages, and an arena allocator. Against the YAML Test Suite, it produces the exact expected event stream for all 308 valid-YAML cases and rejects all 94 invalid-YAML cases, in both parser modes (see Conformance and Testing). It parses 2–14× faster than libyaml and libfyaml (see Performance).
One deliberate leniency: a line inside a flow collection or multi-line quoted scalar that starts with the closing bracket or quote may sit at any indentation, so the common style
key: [
a,
b
]
parses, although strict YAML 1.2 would reject the unindented ].
make # build liboyl.a
make test # run scanner unit tests
make test-schema # run schema/tag resolution tests
make test-emitter # run emitter tests
make test-merge # run merge key tests
make test-resolve # run alias resolution tests
make test-errors # run error handling tests
make test-flow # run flow collection and parser edge-case tests
make test-suite # run YAML Test Suite (requires git submodules)
make test-all # run all tests
Requires a C11 compiler. Tested with GCC and Clang on Linux and macOS,
and with MSVC on Windows (x64, x86 and ARM64). The Makefile needs a
Unix shell; with MSVC, compile src/*.c with /std:c11 /Iinclude into
a library, as CI does (.github/workflows/ci.yml).
#include "oyl/oyl.h"
const char *yaml = "greeting: hello\nitems:\n - one\n - two\n";
oyl_arena *arena = oyl_arena_new(4096);
oyl_parser *parser = oyl_parser_new(yaml, strlen(yaml), arena);
const oyl_event *evt;
while (oyl_parse_next(parser, &evt) == OYL_OK) {
if (evt->type == OYL_EVT_STREAM_END) break;
printf("%s", oyl_event_type_str(evt->type));
if (evt->anchor.data) printf(" &%.*s", (int)evt->anchor.len, evt->anchor.data);
if (evt->tag.data) printf(" <%.*s>", (int)evt->tag.len, evt->tag.data);
if (evt->type == OYL_EVT_SCALAR)
printf(" %.*s", (int)evt->value.len, evt->value.data);
printf("\n");
}
oyl_parser_free(parser);
oyl_arena_free(arena);
oyl_parse_next hands back a pointer to an event owned by the parser,
valid until the next call. Its string fields (value, anchor, tag)
point into your input or the arena and stay valid until those are freed,
so they can be kept without copying.
oyl_arena *arena = oyl_arena_new(4096);
oyl_str data = oyl_read_file("config.yaml", arena);
if (data.data) {
oyl_parser *p = oyl_parser_new(data.data, data.len, arena);
/* ... parse ... */
oyl_parser_free(p);
}
oyl_arena_free(arena); /* frees file data too */
oyl_arena *arena = oyl_arena_new(4096);
const char *yaml = "greeting: hello\nitems:\n - one\n - two\n";
/* parse */
oyl_parser *p = oyl_parser_new(yaml, strlen(yaml), arena);
/* emit */
oyl_emitter *e = oyl_emitter_new(arena);
const oyl_event *evt;
while (oyl_parse_next(p, &evt) == OYL_OK) {
oyl_emit(e, evt);
if (evt->type == OYL_EVT_STREAM_END) break;
}
oyl_str out = oyl_emitter_output(e);
fwrite(out.data, 1, out.len, stdout);
oyl_emitter_free(e);
oyl_parser_free(p);
oyl_arena_free(arena);
oyl_emitter *e = oyl_emitter_new(arena);
oyl_emit_stream_start(e);
oyl_emit_document_start(e, true); /* implicit: no "---" */
oyl_emit_mapping_start(e, OYL_STR_NULL, OYL_STR_NULL, false);
oyl_emit_scalar(e, OYL_STR_LIT("name"), OYL_SCALAR_PLAIN, OYL_STR_NULL, OYL_STR_NULL);
oyl_emit_scalar(e, OYL_STR_LIT("oyl"), OYL_SCALAR_PLAIN, OYL_STR_NULL, OYL_STR_NULL);
oyl_emit_mapping_end(e);
oyl_emit_document_end(e, true);
oyl_emit_stream_end(e);
oyl_str out = oyl_emitter_output(e); /* "name: oyl\n" */
For lower-level access, the scanner produces a flat token stream without synthetic block structure tokens:
oyl_scanner *scanner = oyl_scanner_new(yaml, len, arena);
const oyl_token *tok;
while (oyl_scan_next(scanner, &tok) == OYL_OK) {
if (tok->type == OYL_TOK_STREAM_END) break;
printf("%-20s %.*s\n",
oyl_token_type_str(tok->type),
(int)tok->value.len, tok->value.data);
}
oyl_scanner_free(scanner);
| Type | Description |
|---|---|
oyl_str | Non-owning string view (data, len) |
oyl_mark | Source position (offset, line, col) |
oyl_token | Scanner output (type, value, scalar_style, start, end), owned by the scanner |
oyl_event | Parser output (type, value, anchor, tag, scalar_style, flow), owned by the parser |
oyl_schema | Opaque tag resolution schema (failsafe, JSON, core, or custom) |
oyl_emitter | Writes YAML from events or trees, configured with setters |
oyl_tree | A document in memory: an array of oyl_node in document order |
Tokens and events are only ever allocated by the library and read through
const pointers, and configuration goes through functions, so new fields
and options can be added without breaking compiled programs.
| Event | Meaning |
|---|---|
STREAM_START / STREAM_END | Document stream boundaries |
DOC_START / DOC_END | Document boundaries (--- / ...) |
MAPPING_START / MAPPING_END | Mapping (key-value pairs) |
SEQUENCE_START / SEQUENCE_END | Sequence (list) |
SCALAR | Scalar value |
ALIAS | Alias reference (*name) |
The emitter converts an event stream back into YAML text that parses back
to the same data: every valid YAML Test Suite case round-trips (parse,
emit, parse) with identical events in all three styles, and the fuzzer
checks the same property on arbitrary input. Block scalars are written as
exact literals, collections used as keys (and keys longer than YAML allows
an implicit key) take the explicit ? key form, and tags are written in a
form that reads back unchanged. Events out of order are an error
(OYL_ERR_EMIT), and the first error is final. Three output styles are
available:
| Style | Description |
|---|---|
OYL_EMIT_BLOCK | Indented block style (default) |
OYL_EMIT_FLOW | Flow style ({key: value}, [a, b]) |
OYL_EMIT_MINIMAL | Compact flow with minimal whitespace |
The default is block style with a 2-space indent. Change it with setters:
oyl_emitter *e = oyl_emitter_new(arena);
oyl_emitter_set_style(e, OYL_EMIT_FLOW);
oyl_emitter_set_indent(e, 4); /* spaces per indent level, 1-10 */
Plain scalars are written plain whenever that reads back as the same text,
so 8080, true and null keep their meaning; text that would be misread
(: or # inside, leading indicators, line breaks, ...) is quoted. A
scalar tagged !!str whose text looks like a number or keyword is quoted to
stay a string. Characters YAML doesn't allow unescaped (control characters,
C1 controls, U+FEFF, U+FFFE, U+FFFF), and NEL, U+2028 and U+2029, which
YAML 1.1 readers take as line breaks, are escaped in double quotes.
The emitter writes YAML 1.2, and no %YAML line unless given one: a
document without one is YAML 1.2 to a YAML 1.2 reader, and go-yaml v2 and
v3 reject any but %YAML 1.1. A document's own directives aren't among
its events, so they're given separately: oyl_emit_yaml_directive and
oyl_emit_tag_directive before its start, with what
oyl_parser_yaml_version and oyl_parser_tag_directive give for a parsed
one (see docs/api). oyl_emit_tree writes a
tree's own. A %YAML 1.1 document re-emitted with its directive reads
back by YAML 1.1's rules, as it was read.
Enable merge key expansion (<<) to inherit keys from anchored mappings:
oyl_parser_set_merge(parser, true);
defaults: &defaults
adapter: postgres
host: localhost
production:
<<: *defaults
database: mydb
With merge enabled, production expands to contain adapter, host, and
database as direct entries. Explicit keys override merged ones. The merge
value can be an alias to a mapping, an inline mapping (<<: {k: v}), or a
sequence of those, also through an alias, which merges all of them (first
wins on conflicts). Of two << in one mapping, the later wins, as in
PyYAML. Any other value, or an alias to an undefined anchor, is a parse
error. Quoted "<<" is treated as a normal key.
Enable alias resolution to expand *alias references inline instead of
emitting ALIAS events:
oyl_parser_set_resolve(parser, true);
Scalar, mapping, and sequence aliases are all expanded. An alias refers to
the most recent anchor of that name before it in the same document, so a
reused anchor name works as in the spec. Circular references, and aliases
with no preceding anchor (undefined, forward, or from an earlier document),
are kept as ALIAS events (no error, no infinite loop). Combines with merge
keys when both are enabled.
The parser enforces limits against hostile or runaway input. Exceeding any
of them stops parsing with OYL_ERR_LIMIT and an error message.
Event count. Defaults to 10,000 events. This is sufficient for typical config files (roughly 100-200KB of dense YAML), but large documents may need a higher limit:
oyl_parser_set_max_events(parser, 100000); /* raise for large files */
oyl_parser_set_max_events(parser, 0); /* disable limit entirely */
Each YAML node produces 1-3 events (a key-value pair is ~2 events, plus structure start/end events), so the default 10,000 events handles roughly 3,000-5,000 nodes. Documents with large string values use fewer events per byte and can go well beyond 200KB at the default limit.
Nesting depth. Defaults to 256 levels of nested collections. It applies to the events delivered, so it also holds when alias or merge expansion nests deeper than the input does:
oyl_parser_set_max_depth(parser, 1000);
Setting it to 0 disables the limit, but inputs using tags, anchors, merge keys, or alias resolution are parsed recursively (roughly 1 KB of stack per level), so very deep input can then overflow the stack.
Alias and merge expansion. Expanding aliases can grow a small document exponentially (the "billion laughs" attack). The expanded stream must fit within the event limit; if that is disabled, expansion is capped at 16 times the unexpanded stream plus 100,000 events. Working out merges, for events and for trees, is bounded at four times that, counting each pair looked at and each item of a merged sequence.
Hash flooding. Anchors, keys and merged mappings are found through hash tables. They hash with SipHash-1-3 under a secret key that each parser takes from the system's randomness, so no document can be written whose entries all collide and slow every lookup. Nothing else depends on the key: output is the same from run to run.
Both the scanner and parser provide error messages with source locations:
oyl_parser *p = oyl_parser_new(yaml, strlen(yaml), arena);
const oyl_event *evt;
if (oyl_parse_next(p, &evt) != OYL_OK) {
const char *msg = oyl_parser_error(p);
oyl_mark mark = oyl_parser_error_mark(p);
fprintf(stderr, "error: line %zu col %zu: %s\n",
mark.line, mark.col, msg);
}
Scanner errors are available directly or propagate through the parser:
const char *msg = oyl_scanner_error(scanner);
oyl_mark mark = oyl_scanner_error_mark(scanner);
Oyl supports pluggable tag resolution per YAML 1.2 Chapter 10. Three built-in schemas ship as presets:
| Schema | Resolves |
|---|---|
| Failsafe | Everything is !!str / !!seq / !!map |
| JSON | null, true/false, integers, floats |
| Core | JSON + Null/NULL/~, True/TRUE/False/FALSE, 0x/0o ints |
| go-yaml v2 | As Kubernetes reads YAML: yes/no/on/off booleans, 0777 octal, 0b binary, 1_000, timestamps |
The JSON preset shares Core's number rules, so it is looser than the YAML
1.2 JSON schema (section 10.2): +1, 01, 0x1F, 0o17, .inf and .nan
resolve as numbers, and other plain scalars as !!str rather than an
error.
Schema is opt-in -- without oyl_parser_set_schema(), scalars have no tag.
oyl_parser_set_schema(parser, oyl_schema_core());
/* events now carry resolved tags:
* "true" -> tag:yaml.org,2002:bool
* "42" -> tag:yaml.org,2002:int
* "hello" -> tag:yaml.org,2002:str
* "null" -> tag:yaml.org,2002:null
* quoted scalars always resolve to !!str
* explicit tags (!!str, !foo) are never overwritten */
A schema also gives a scalar's value, parsed by the rule that typed it:
oyl_value v;
oyl_schema_value(oyl_schema_goyaml2(), evt, &v); /* "0777" -> OYL_VALUE_INT, 511 */
Build your own schema to support YAML 1.1 booleans (yes/no/on/off)
or any other resolution rules:
oyl_schema_builder *b = oyl_schema_builder_new(arena);
const char *trues[] = {"true","True","TRUE","yes","Yes","YES","on","On","ON"};
const char *falses[] = {"false","False","FALSE","no","No","NO","off","Off","OFF"};
oyl_schema_builder_add_bools(b, trues, 9, falses, 9);
const char *nulls[] = {"null","Null","NULL","~",""};
oyl_schema_builder_add_nulls(b, nulls, 5);
oyl_schema_builder_add_int(b); /* 42, 0xFF, 0o77 */
oyl_schema_builder_add_float(b); /* 3.14, .inf, .nan */
const oyl_schema *schema = oyl_schema_builder_finish(b); /* lives in the arena */
oyl_schema_builder_free(b);
oyl_parser_set_schema(parser, schema);
Rules are matched in order (first match wins). Match types: OYL_MATCH_EXACT,
OYL_MATCH_ICASE, and OYL_MATCH_BUILTIN (procedural int/float matchers).
Each rule is a tag bound to a parser: the int and float parsers take
dialect flags (oyl_schema_builder_add_int_flags, ..._float_flags), and
oyl_schema_builder_add_type binds any tag, a built-in one included, to
your own parser. See docs/api.
A dialect is what varies between YAML worlds: a schema (types and
values), syntax leniencies, and how << merges. Each is a dialect of one
YAML version. Each document's directives choose its dialect, among those
the program allows:
%TAG !! <prefix>: the dialect registered for that prefix types the
document's explicit !! tags and its untagged values alike%YAML <version>: the program's dialect for that version, the one
registered for it or else the default if it is of that version; else
the built-in one: 1.1 with PyYAML's types (yes is true, 0777 is
octal) and merge, so a document PyYAML reads gets the same values
(oyl.h lists the few exceptions), 1.2 by the Core schemaThe document picks the version, the program the dialect for each: with
go-yaml v2 as the default, %YAML 1.1 keeps go-yaml v2, a dialect of 1.1,
and with a schema set, %YAML 1.2 keeps that schema.
oyl_parser_set_dialect(p, oyl_dialect_goyaml2()); /* the default */
oyl_parser_add_dialect(p, my_dialect, NULL, "tag:acme.com,2026:");
Built in: oyl_dialect_yaml12(), oyl_dialect_yaml11() and
oyl_dialect_goyaml2() (go-yaml v2's types and merge order, with its
leniencies: \' in double quotes, %YAML 1.1 only, and no line feed for a
block scalar ending the input, as in libyaml). oyl_dialect_new builds one
of any version from any schema.
YAML 1.2 applies a %TAG !! prefix to explicit tags only; Oyl applies it
to untagged values too, which is what !! was meant for in YAML 1.2's
design: ! for the application's types, !! for the underlying schema.
Warnings go to a callback (oyl_parser_set_warn): a %YAML 1.x version
other than 1.1 or 1.2, a %TAG !! prefix with no dialect, each leniency a
dialect used, and in go-yaml v2's merge order a merge overriding a key
written before it.
oyl_parse_json reads a document and writes it as JSON. With the go-yaml
v2 dialect, the output is byte for byte what Kubernetes gets from
sigs.k8s.io/yaml's YAMLToJSON, about seven times faster on real
manifests:
oyl_parser *p = oyl_parser_new(yaml, len, arena);
oyl_parser_set_dialect(p, oyl_dialect_goyaml2());
oyl_str json;
if (oyl_parse_json(p, NULL, 0, &json) == OYL_OK && json.data) /* NULL: the document's dialect */
puts(json.data); /* {"apiVersion":"v1","kind":"Pod",...} */
Keys are sorted, numbers and strings are written as Go's encoding/json
writes them, aliases are expanded, and << merges follow go-yaml v2's
order (a merge overrides the keys before it). For standard JSON use the
Core schema and OYL_JSON_MERGE, YAML's merge, where keys written in the
mapping win. Each call reads one document; at the end of the stream the
result is empty.
The go-yaml v2 dialect takes on go-yaml v2's leniencies that occur in real
manifests. Others it doesn't copy: ?x as an explicit key, anchor names
cut at :, and everything after the first document's root node ignored.
oyl_parse_tree reads a document into memory: lookups, iteration and
typed values, without handling events yourself.
oyl_parser *p = oyl_parser_new(yaml, len, arena);
oyl_tree *t;
if (oyl_parse_tree(p, &t) == OYL_OK && t) {
uint32_t spec = oyl_node_get(t, 0, OYL_STR_LIT("spec")); /* 0 is the root */
uint32_t replicas = oyl_node_get(t, spec, OYL_STR_LIT("replicas"));
oyl_value v;
if (oyl_node_value(t, replicas, &v) == OYL_OK && v.kind == OYL_VALUE_INT)
printf("%lld replicas\n", (long long)v.as.i);
oyl_tree_free(t);
}
A tree is one array of 32-byte nodes in document order. A collection
records the size of its subtree, so its first child is the next node and
the next sibling of node i is i + span; a mapping's children alternate
key, value. Walking needs no function calls:
uint32_t count;
const oyl_node *n = oyl_tree_nodes(t, &count);
for (uint32_t c = 0, k = m + 1; c < n[m].len; c++) { /* the pairs of mapping m */
uint32_t v = k + n[k].span; /* n[k] is a key, n[v] its value */
k = v + n[v].span;
}
Nodes hold offsets, not pointers: text stays in the input, except text
the parser rewrote (escapes, folded lines), which goes into the tree's
pool. So a binding can copy a tree's arrays whole, or read them in place.
Each plain scalar is typed while the tree is built, by the document's
dialect. Aliases and << merge keys stay as written: oyl_node_get
follows aliases and applies merges in the dialect's order, and
oyl_node_entries gives a mapping's pairs with merges applied. The
merges are worked out once as the tree is built, so reading a mapping
costs only its pairs, however the merges nest. Keys are
equal as YAML compares them, by tag and value: 0x50 and 80 are one
key, 80 and "80" two. oyl_node_get takes a key's text ("80"
finds the string, else 80), oyl_node_get_value its value. A finished
tree is read-only, so threads can share it. The input, and a schema or
dialect given to the parser, must outlive the tree; the parser need not.
Each call reads one document; at the end of the stream the tree is NULL.
oyl_emit_tree writes a tree back out as YAML, as emitting its
document's events would.
Input ──> Scanner ──> Parser ──> Emitter ──> Output
\ | /
└─── Arena (memory) ─┘
Scanner : flat token stream (SIMD-accelerated)
Parser : event stream (indent tracking, block structure,
merge keys, alias resolution)
Emitter : YAML text output (block / flow / minimal)
Arena : bump allocator backing all components
The scanner is intentionally "pure" -- it produces raw tokens without synthetic block start/end markers. The parser layer handles indent-based block structure, simple key resolution, and property (anchor/tag) attachment.
Parse throughput in MB/s (higher is better), measured with
make bench-compare on an Intel Core Ultra 7 155H, one core, GCC 16.2
-O2, median of 11 runs in each of 3 passes (passes agree within 5%), October 2026.
Every library parses the same bytes:
| Input | Oyl | libyaml 0.2.5 | libfyaml 0.9.6 | rapidyaml 0.16 |
|---|---|---|---|---|
| Generated, 10 MB | ||||
| block mappings and sequences | 314 | 73 | 61 | 245 |
| mixed block, flow and quoted | 311 | 84 | 73 | 279 |
| JSON | 531 | 69 | 58 | 324 |
| config files (comments, block scalars, anchors) | 419 | 120 | 102 | 375 |
| Real files, repeated to ~10 MB | ||||
| travis.yml | 836 | 144 | 178 | 428 |
| appveyor.yml | 580 | 128 | 145 | 422 |
| compile_commands.json | 2701 | 217 | 294 | 1199 |
| Scalar-heavy text | ||||
literal block scalars (|) | 3218 | 276 | 282 | 972 |
folded block scalars (>) | 2108 | 279 | 282 | 878 |
| multi-line double-quoted | 3105 | 214 | 327 | 350 |
All four are measured as event parsers: Oyl, libyaml and libfyaml as they
are, and rapidyaml through its event parser (EventHandlerInts, with
buffers reused between parses, its fastest mode) rather than its usual API,
which builds a tree and so does more work. The real files and scalar-heavy
inputs are the test files from rapidyaml's own benchmark suite; all timings
were measured here, on the machine above, with make bench-compare.
Oyl is 3.5–9.2× faster than libyaml and libfyaml on the generated inputs, 4–12× on the real files and 2.4–14.5× on scalar-heavy input. Against rapidyaml's event parser it is 1.1–1.6× faster on the generated inputs, which are mostly short keys and values, 1.4–2.3× on the real files and 2.4–8.9× on long scalars.
Where the speed comes from:
key: value
lines in block mappings, and one-line flow collections of plain and
escape-free double-quoted scalars, as JSON's are. Events are built where
they are handed out, never copied.%YAML and %TAG directives,
and properties on a key use an eager parse of each document instead.Throughput in MB/s building each library's tree, measured with
make bench-compare-tree on the same machine and compiler, median of 11
runs in each of 3 passes (passes agree within 6%, except libyaml on the
CRD, 8%, and rapidyaml building a new tree each time, up to 28% on the
generated inputs), October 2026:
| Input | Oyl | libyaml 0.2.5 | libfyaml 0.9.6 | rapidyaml 0.16 | rapidyaml, reused |
|---|---|---|---|---|---|
| Generated, 10 MB | |||||
| block mappings and sequences | 167 | 44 | 32 | 104 | 166 |
| mixed block, flow and quoted | 176 | 56 | 40 | 112 | 189 |
| JSON | 249 | 46 | 34 | 113 | 197 |
| config files (comments, block scalars, anchors) | 301 | 82 | 63 | 182 | 278 |
| Real files, repeated to ~10 MB | |||||
| travis.yml | 637 | 101 | 109 | 195 | 284 |
| appveyor.yml | 434 | 89 | 87 | 194 | 294 |
| compile_commands.json | 2258 | 180 | 246 | 827 | 1105 |
| Kubernetes | |||||
| 854 manifests in one stream, 7.3 MB | 566 | fails¹ | 134 | fails² | fails² |
Each library builds its own tree: oyl_parse_tree, libyaml's
yaml_parser_load, libfyaml's fy_parse_load_document (with duplicate
keys allowed, as the others allow them), and rapidyaml's parse_in_place,
with a new tree for each parse like the others, and with its parser and
tree reused and reserved to size, its fastest mode. The Kubernetes inputs
are public manifests (kubernetes/examples, the kubernetes.io examples, and
the Argo CD, cert-manager and Prometheus Operator install bundles), one of
them left out for an escape YAML 1.2 doesn't have (\').
¹ libyaml implements YAML 1.1, which doesn't allow a document to follow
... without ---. ² rapidyaml's trees can't hold a collection as a key,
as in a Helm template's config: {{config_data}}.
Oyl builds trees 3.1–13× faster than libyaml and 2.4–10× faster than libfyaml, and 1.6–8.1× faster than rapidyaml building a new tree each time. rapidyaml reusing its buffers is even with Oyl on the generated block mappings and 7% faster on the mixed input; Oyl is 1.1–1.3× faster on the config files and JSON, and 1.5–7.8× on the real files and long scalars.
The memory the trees hold after parsing, in MB per MB of YAML:
| Input | Oyl | libyaml | libfyaml | rapidyaml |
|---|---|---|---|---|
| block mappings and sequences | 6.5 | 33 | 74 | 14 |
| JSON | 6.5 | 32 | 67 | 14 |
| config files | 7.3 | 15 | 37 | 7.2 |
| travis.yml | 2.7 | 11 | 20 | 7.2 |
| Kubernetes manifests | 1.9 | fails | 12 | fails |
| the largest CRD | 1.5 | 7.6 | 15 | 3.4 |
| literal block scalars | 2.0 | 1.9 | 1.0 | 0.22 |
| multi-line plain | 1.1 | 1.9 | 1.0 | 0.22 |
An Oyl node is 32 bytes and points into the input, so on structured input
its trees are 2–3× smaller than rapidyaml's (the same on the config
files), 2–5× smaller than libyaml's and 5–11× smaller than libfyaml's.
Text the parser rewrites (escapes, folded lines) is copied into the tree,
while rapidyaml rewrites the input buffer in place and libfyaml decodes
text when it's read, so on long scalars they hold less. Measured from
glibc's heap (mallinfo2) with the trees of the whole input kept.
Throughput in MB/s of input writing each library's trees, built
beforehand, back out as YAML, measured with make bench-compare-emit the
same way (passes agree within 8%):
| Input | Oyl | libyaml 0.2.5 | libfyaml 0.9.6 | rapidyaml 0.16 |
|---|---|---|---|---|
| Generated, 10 MB | ||||
| block mappings and sequences | 323 | 68 | 93 | 373 |
| mixed block, flow and quoted | 399 | 85 | 114 | 354 |
| JSON | 389 | 66 | 87 | 356 |
| config files (comments, block scalars, anchors) | 608 | 137 | 124 | 550 |
| Real files, repeated to ~10 MB | ||||
| travis.yml | 1118 | 206 | 351 | 881 |
| appveyor.yml | 716 | 182 | 204 | 740 |
| compile_commands.json | 3061 | 247 | 646 | 740 |
| Kubernetes | ||||
| 854 manifests in one stream, 7.3 MB | 1373 | fails¹ | 179 | fails² |
| the largest CRD, 1.4 MB | 1482 | 317 | 511 | 1687 |
| Scalar-heavy text | ||||
| literal block scalars () |
Each library writes in its default style: oyl_emit_tree, libyaml's
yaml_emitter_dump (which consumes its documents, so they're loaded again
before each run, untimed), libfyaml's fy_emit_document_to_string, and
rapidyaml's emitrs_yaml. ¹ ² As above: the trees can't be built.
Oyl writes YAML 3.9–12× faster than libyaml and 3–31× faster than libfyaml. Against rapidyaml it's faster on 10 of the 13 inputs both can read (up to 4.7×, on double-quoted text), and slower on 3: block mappings by 13%, the CRD by 12% and appveyor.yml by 3%.
Run make bench to measure Oyl alone. make bench-compare runs the event
table above with whichever libraries are installed,
make bench-compare-tree the tree tables and make bench-compare-emit
the emitter's; see bench/compare/run.sh for adding rapidyaml
(RYML_HEADER, RYML_SRC) and your own files (CASES).
Graded on the YAML Test Suite (v2022-01-17-9-gda267a5c; 308 valid and 94 invalid cases) by one script that compares each parser's events with the suite's:
| Parser | Valid (308) | Invalid rejected (94) |
|---|---|---|
| Oyl | 308 | 94 |
| libfyaml 0.9.6 | 308 | 94 |
rapidyaml 0.16, built with RYML_WITH_TAB_TOKENS | 299 | 93 |
| rapidyaml 0.16, default build | 293 | 90 |
| libyaml 0.2.5 | 252 | 78 |
rapidyaml's default build rejects tabs after : and - (a documented
choice), and its event output leaves %TAG shorthands unexpanded, which
accounts for 9 of its differences. libyaml implements YAML 1.1.
Oyl's own suite runner (make test-suite) also checks both of its parser
modes and round-trips every valid case through the emitter in all three
output styles. A libFuzzer harness (fuzz/fuzz_parser.c) checks that
arbitrary input never crashes or exceeds the safety limits, and that
whatever parses, emits and parses again to the same data. It runs for 3
minutes on every push, and
ClusterFuzzLite runs it for
hours a day under AddressSanitizer and UndefinedBehaviorSanitizer. CI
builds and tests with GCC and Clang on Linux and macOS, and with MSVC on
Windows.
MIT
| multi-line single-quoted | 2873 | 216 | 329 | 366 |
| multi-line plain | 994 | 198 | 421 | 299 |
| the largest CRD, 1.4 MB |
| 625 |
| 161 |
| 148 |
| 274 |
| 394 |
| Scalar-heavy text |
literal block scalars (|) | 2733 | 254 | 268 | 876 | 973 |
folded block scalars (>) | 1880 | 255 | 265 | 804 | 885 |
| multi-line double-quoted | 2659 | 200 | 306 | 329 | 342 |
| multi-line single-quoted | 2431 | 203 | 307 | 358 | 368 |
| multi-line plain | 940 | 190 | 389 | 290 | 301 |
|| 1994 |
| 270 |
| 96 |
| 1371 |
folded block scalars (>) | 2378 | 250 | 249 | 1706 |
| multi-line double-quoted | 2791 | 249 | 89 | 593 |
| multi-line single-quoted | 2305 | 245 | 94 | 1303 |
| multi-line plain | 2281 | 249 | 96 | 1948 |