YaFF (Yet another Flat Format)

repository·main·Indexed 19 days ago

https://github.com/yandex/yaff

A high-performance C++ serialization library providing a zero-copy wire format for the Protobuf ecosystem. YaFF uses .proto schemas as a single source of truth while eliminating parsing overhead via mmap-compatible layouts. It features adaptive layout strategies, including Flat Layout for high-density data and Sparse Layout for low-to-medium density data, to achieve near-native C++ struct read performance.

Tokens
20.3K
Snippets
46
Records
87
Agent score
67%

What's inside YaFF

  1. What is YaFF and when should you use it?

    main

    YaFF (Yet another Flat Format) is a high-performance C++ serialization library designed to provide a zero-copy wire format for the Protobuf ecosystem. It uses .proto files as the single source of truth but changes the physical data representation to eliminate runtime overhead.

    Key Characteristics

    • Zero-copy: No parsing or copying is required on read; data can be read directly from memory-mapped files.
    • Protobuf Interoperability: Maintains compatibility with the Protobuf ecosystem, allowing you to use existing .proto schemas.
    • High Performance: Uses adaptive layout strategies to bring field access performance close to native C++ structs.

    Use Cases

    Use YaFF if you:

    • Want faster reads while staying within the Protobuf ecosystem.
    • Need the lowest possible read overhead in performance-critical (hot) paths.
    • Require zero-copy benefits (e.g., reading directly from a buffer or memory-mapped file).
    • Control both the producer and consumer sides of the data pipeline within a trusted system.
  2. What is YaFF (Yet another Flat Format)?

    main

    YaFF is a high-performance C++ serialization library designed to provide a zero-copy wire format for the Protobuf ecosystem.

    Core Concepts:

    • Single Source of Truth: Uses existing .proto files for schema definition.
    • Zero-Copy: Eliminates runtime parsing overhead by using mmap-compatible layouts, allowing direct access to data.
    • Adaptive Layouts: Decouples schema definitions from physical memory layout, allowing for different runtime representations (e.g., optimized for speed, memory, or analytics) without changing the source schema.
    • Interoperability: Supports two-way message conversion, allowing standard Protobuf components to parse YaFF wire formats.
  3. Use Schema Slices for lightweight data representations

    main

    Schema slices allow you to generate multiple independent runtime representations from a single .proto file. This is useful for shipping lightweight buffers to consumers that only need a subset of the data.

    To define a slice, you must annotate every message and field along the path from the root to the data using (yaff.proto.message) and (yaff.proto.field) options.

    Key Rules:

    • slice_name: Defines which slice the option applies to. If omitted, the field/message is included in all slices.
    • id: If omitted in a slice, the original Protobuf number is preserved.
    • Resolution: Options are resolved slice-first. An option tagged with a specific slice_name takes precedence over an untagged fallback.
    • Generation: Slices are selected at generation time using the TAG and NAMESPACE parameters of yaff_generate.
    message Embedding {
        option (yaff.proto.message) = {slice_name: "banner_light"};
        option (yaff.proto.message) = {slice_name: "banner_heavy"};
    
        // Shared across both slices using its original Protobuf field number
        optional uint64 vector_id = 1 [
            (yaff.proto.field) = {slice_name: "banner_light"},
            (yaff.proto.field) = {slice_name: "banner_heavy"}
        ];
    
        optional uint64 model_version = 2;
    
        // Included only in the heavy slice, with an overridden YaFF identifier
        optional uint64 computed_time = 3 [
            (yaff.proto.field) = {slice_name: "banner_heavy", id: 2}
        ];
    }
  4. Manage field numbers and reserved values

    main

    YaFF inherits field numbers from your .proto file. These numbers serve as the unique identifiers for fields within a message.

    Best Practices

    • Uniqueness: Field numbers must be unique within a message and must never be reused once assigned.
    • Optimization: Assign the smallest numbers to fields that are populated most frequently to reduce overhead in certain layouts.
    • Gaps: While gaps between numbers work, they may add overhead in some layouts. You can reclaim space by reassigning compact IDs.

    Retiring Fields

    You can retire a field in three ways, which affects available layouts:

    1. [deprecated = true]: Keeps the field in the schema and hides it from generated YaFF interfaces. This preserves all layout options.
    2. reserved: Removes the field and its type information. This may limit available layouts.
    3. Deleting the field: Behaves like reserved. Avoid this to prevent accidental reuse of field numbers.

    If you use reserved but need to keep all layout options open, you can explicitly provide the missing type information to YaFF (see the Advanced section in documentation).

  5. Understand the two parts of YaFF C++ generated code

    main

    The YaFF compiler produces two distinct types of C++ code from .proto files:

    1. Serialization and deserialization: Tools to convert between Protobuf messages and the YaFF wire format.
    2. Zero-copy reading: A proto-like, immutable interface that reads fields directly from the YaFF representation without a parsing step. This is the high-performance path.

    Note on Mutability: The reading interface is immutable. To modify a message, you must follow the pattern: deserialize the YaFF representation into a Protobuf message, modify the Protobuf message, and then serialize it back to YaFF.

  6. Understand the trade-offs between Flat and Sparse layouts

    main

    The serialized size of a YaFF message is determined by metadata overhead and how unset fields are encoded.

    Flat Layout Behavior

    In a Flat Layout, setting field N requires writing every field from 0 to N.

    • At full density: It is the most compact format (approx. 25% smaller than FlatBuffers for 5-field structures).
    • At low density: It carries high overhead because it still stores the space for fields up to the last one set, leading to a 'plateau' in size regardless of how many intermediate fields are unset.

    Sparse Layout Behavior

    In a Sparse Layout, setting field N writes metadata for fields 0 to N, but does not write values for unset fields.

    • At low density: It is much more efficient than Flat Layout. Its lead over FlatBuffers widens as data thins out (reaching ~39% savings at 5% density).
    • At high density: It sits between Flat Layout and FlatBuffers.

    Key Advantage: Dynamic Layout

    Because YaFF supports Dynamic Layouts, you are not forced to commit to a single tradeoff in your schema. The runtime can select the optimal layout (Flat or Sparse) during serialization based on the actual data being written.

  7. Understand field presence in YaFF

    main

    Field presence determines whether a field can be distinguished from its default value.

    • Implicit Presence: The field does not track if it was set. A value equal to the default is indistinguishable from an unset field. repeated and map fields always use implicit presence.
    • Explicit Presence: The field tracks whether it was set. This allows distinguishing between a default value and an unset field. Message fields and oneof members always use explicit presence. In proto3, singular scalars can be marked optional to enable explicit presence.

    Explicit-presence fields generate matching accessors (e.g., has_* methods). The performance cost of explicit presence depends on the chosen message layout.

  8. Choose between Local and Random Access Patterns

    main

    Performance characteristics in YaFF vary based on the access pattern of your application:

    Local Access (Hot)

    • Definition: Many fields are read from a single message (small working set, e.g., ~2 KB).
    • Characteristics: Performance is dominated by the format's per-access overhead and the efficiency of the generated code.
    • YaFF Advantage: YaFF's Flat Layout is highly optimized for this, performing significantly faster than Protobuf and FlatBuffers because it minimizes the cost of navigating the structure.

    Random Access (Cold)

    • Definition: A few fields are read from many different messages (large working set, e.g., ~512 MB).
    • Characteristics: Performance is dominated by the cost of the initial cache miss (reaching RAM). The layout's ability to keep data within a single cache line or avoid extra metadata lookups is critical.
    • YaFF Advantage: YaFF stays close to the raw C++ baseline because its layouts minimize the number of unpredictable memory misses required to reach a field.
  9. Understand YaFF message size limits

    main

    The maximum size for a single serialized YaFF message is 2 GiB.

    If your dataset or collection exceeds this size, you should partition the data into smaller blocks (each $\le$ 2 GiB). This approach ensures bounded memory usage and enables concurrent processing by consumers.

  10. Sparse Layout

    main

    The Sparse Layout uses a meta table to address fields, decoupling field numbers from physical positions.

    • Performance: 4 reads and 2 branches per field access.
    • Overhead: 6 bytes per message, plus 1–2 bytes in the table for each field number used (including gaps in the ID range).
    • Schema Evolution: Unrestricted. You can add or remove fields, including scattered field numbers or gaps, without constraints on typing or position.
    • Features: Supports default values, implicit and explicit presence, and is self-describing.
  11. How YaFF layouts work

    main

    A layout determines the physical storage representation of a message in a buffer. While the schema and generated interfaces remain the same, the layout affects performance, memory overhead, and schema evolution capabilities.

    YaFF uses two dispatch modes:

    • Static dispatch: The layout is fixed at compile time. This is the fastest read path because the generated code accesses fields directly without runtime decisions. You enable this by pinning a message to a layout via options.
    • Dynamic dispatch (Default): The layout is resolved at runtime. This allows messages to be adaptable and schemas to evolve. The dispatch mechanism is optimized to provide zero overhead for the fastest layout paths and minimal overhead for others.