diff --git a/docs/mkdocs/docs/api/ordered_json.md b/docs/mkdocs/docs/api/ordered_json.md index b28fe36f6..feea642cd 100644 --- a/docs/mkdocs/docs/api/ordered_json.md +++ b/docs/mkdocs/docs/api/ordered_json.md @@ -13,6 +13,12 @@ Therefore, adding object elements can yield a reallocation in which case all ite [`end()`](basic_json/end.md) iterator) and all references to the elements are invalidated. Also, any iterator or reference after the insertion point will point to the same index, which is now a different value. +## Complexity + +[`ordered_map`](ordered_map.md) has no lookup index: every key-based object operation is a linear scan, so building or +parsing an object of `n` keys costs O(n²) rather than O(n log n). See +[`ordered_map` complexity](ordered_map.md#complexity) for the per-operation table and for measured numbers. + ## Examples ??? example diff --git a/docs/mkdocs/docs/api/ordered_map.md b/docs/mkdocs/docs/api/ordered_map.md index ca4934161..df21175d0 100644 --- a/docs/mkdocs/docs/api/ordered_map.md +++ b/docs/mkdocs/docs/api/ordered_map.md @@ -56,6 +56,48 @@ std::equal_to<> // since C++14 - **find** - **insert** +## Complexity + +Because the elements are stored in a `std::vector` in insertion order, there is no index to look a key up by. Every +key-based operation performs a **linear scan** over the stored elements. With `n` denoting the number of elements in the +container: + +| Operation | Complexity | Note | +|----------------------------------------|----------------|----------------------------------------------------------| +| **emplace** | O(n) | scans for an existing key, then appends (amortized O(1)) | +| **operator\[\]** | O(n) | delegates to **emplace** (non-const) or **at** (const) | +| **at** | O(n) | throws `#!cpp std::out_of_range` if the key is not found | +| **find** | O(n) | | +| **count** | O(n) | the result is always 0 or 1 | +| **erase(key)** | O(n) | scan, then move the remaining elements one position down | +| **erase(pos)**, **erase(first, last)** | O(n) | moves all elements after the erased range | +| **insert(value)** | O(n) | equivalent to **emplace** | +| **insert(first, last)** | O((n + m) * m) | for `m` inserted elements | + +This differs from `#!cpp std::map`, where the same operations are O(log n). + +!!! warning "Quadratic cost of building large objects" + + Because every insertion scans all elements inserted so far, building an object of `n` distinct keys costs + **O(n²)** in total. This applies to filling an [`ordered_json`](ordered_json.md) object key by key as well as to + parsing one, since the parser inserts each key as it is read. + + The cost is negligible for the object sizes typically found in configuration files or API payloads, but it grows + steeply for machine-generated objects with many thousands of keys. Measured with `-O2 -DNDEBUG` for parsing a flat + object of `n` keys, relative to `#!cpp nlohmann::json` (which uses `#!cpp std::map`): + + | `n` | `json` | `ordered_json` | factor | + |--------|--------|----------------|--------| + | 2000 | 0.7 ms | 3.6 ms | 5× | + | 4000 | 0.8 ms | 14.0 ms | 19× | + | 8000 | 1.6 ms | 67.8 ms | 43× | + | 16 000 | 3.3 ms | 181.6 ms | 54× | + + If key order matters for objects of that size, consider a container with a lookup index, such as + [`tsl::ordered_map`](https://github.com/Tessil/ordered-map) + ([integration](https://github.com/nlohmann/json/issues/546#issuecomment-304447518)), as the object type -- see + [object order](../features/object_order.md). + ## Examples ??? example diff --git a/docs/mkdocs/docs/features/object_order.md b/docs/mkdocs/docs/features/object_order.md index ac5eb7beb..f62474efd 100644 --- a/docs/mkdocs/docs/features/object_order.md +++ b/docs/mkdocs/docs/features/object_order.md @@ -53,6 +53,12 @@ If you do want to preserve the **insertion order**, you can use the type [`nlohm Alternatively, you can use a more sophisticated ordered map like [`tsl::ordered_map`](https://github.com/Tessil/ordered-map) ([integration](https://github.com/nlohmann/json/issues/546#issuecomment-304447518)) or [`nlohmann::fifo_map`](https://github.com/nlohmann/fifo_map) ([integration](https://github.com/nlohmann/json/issues/485#issuecomment-333652309)). +The [`ordered_map`](../api/ordered_map.md) behind `nlohmann::ordered_json` is deliberately minimal and has no lookup +index, so every key access is a linear scan and building an object of `n` keys costs O(n²). This is unnoticeable at +typical object sizes but becomes significant for objects with many thousands of keys; see +[`ordered_map` complexity](../api/ordered_map.md#complexity). The alternatives above keep a lookup index and do not +have this cost. + ### Notes on parsing Note that you also need to call the right [`parse`](../api/basic_json/parse.md) function when reading from a file.