Skip to content

Commit 5d19f24

Browse files
committed
docs: document the complexity of ordered_map operations
ordered_map stores its elements in a std::vector in insertion order and has no lookup index, so emplace, operator[], at, find, count, erase, and insert are all linear scans. The documentation stated no complexity for any operation, neither in ordered_map.md nor in ordered_json.md. Add a per-operation complexity table and note the consequence: building or parsing an ordered_json object of n keys is O(n^2). Measured with -O2 -DNDEBUG for parsing a flat object of n keys, ordered_json is 5x slower than json at n=2000 and 54x slower at n=16000, with the timings quadrupling per doubling of n. Cross-reference the table from ordered_json.md and from the object order page, which recommends ordered_json without mentioning the cost. Signed-off-by: Niels Lohmann <mail@nlohmann.me>
1 parent 68f0722 commit 5d19f24

3 files changed

Lines changed: 54 additions & 0 deletions

File tree

docs/mkdocs/docs/api/ordered_json.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@ Therefore, adding object elements can yield a reallocation in which case all ite
1313
[`end()`](basic_json/end.md) iterator) and all references to the elements are invalidated. Also, any iterator or
1414
reference after the insertion point will point to the same index, which is now a different value.
1515

16+
## Complexity
17+
18+
[`ordered_map`](ordered_map.md) has no lookup index: every key-based object operation is a linear scan, so building or
19+
parsing an object of `n` keys costs O(n²) rather than O(n log n). See
20+
[`ordered_map` complexity](ordered_map.md#complexity) for the per-operation table and for measured numbers.
21+
1622
## Examples
1723

1824
??? example

docs/mkdocs/docs/api/ordered_map.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,48 @@ A minimal map-like container that preserves insertion order for use within [`nlo
2828
The type uses a `std::vector` to store object elements. Therefore, adding elements can yield a reallocation in which
2929
case all iterators (including the `end()` iterator) and all references to the elements are invalidated.
3030
31+
## Complexity
32+
33+
Because the elements are stored in a `std::vector` in insertion order, there is no index to look a key up by. Every
34+
key-based operation performs a **linear scan** over the stored elements. With `n` denoting the number of elements in the
35+
container:
36+
37+
| Operation | Complexity | Note |
38+
|----------------------------------|---------------------------|-----------------------------------------------------------|
39+
| **emplace** | O(n) | scans for an existing key, then appends (amortized O(1)) |
40+
| **operator\[\]** | O(n) | delegates to **emplace** (non-const) or **at** (const) |
41+
| **at** | O(n) | throws `#!cpp std::out_of_range` if the key is not found |
42+
| **find** | O(n) | |
43+
| **count** | O(n) | the result is always 0 or 1 |
44+
| **erase(key)** | O(n) | scan, then move the remaining elements one position down |
45+
| **erase(pos)**, **erase(first, last)** | O(n) | moves all elements after the erased range |
46+
| **insert(value)** | O(n) | equivalent to **emplace** |
47+
| **insert(first, last)** | O((n + m) * m) | for `m` inserted elements |
48+
49+
This differs from `#!cpp std::map`, where the same operations are O(log n).
50+
51+
!!! warning "Quadratic cost of building large objects"
52+
53+
Because every insertion scans all elements inserted so far, building an object of `n` distinct keys costs
54+
**O(n²)** in total. This applies to filling an [`ordered_json`](ordered_json.md) object key by key as well as to
55+
parsing one, since the parser inserts each key as it is read.
56+
57+
The cost is negligible for the object sizes typically found in configuration files or API payloads, but it grows
58+
steeply for machine-generated objects with many thousands of keys. Measured with `-O2 -DNDEBUG` for parsing a flat
59+
object of `n` keys, relative to `#!cpp nlohmann::json` (which uses `#!cpp std::map`):
60+
61+
| `n` | `json` | `ordered_json` | factor |
62+
|--------|--------|----------------|--------|
63+
| 2000 | 0.7 ms | 3.6 ms | 5× |
64+
| 4000 | 0.8 ms | 14.0 ms | 19× |
65+
| 8000 | 1.6 ms | 67.8 ms | 43× |
66+
| 16 000 | 3.3 ms | 181.6 ms | 54× |
67+
68+
If key order matters for objects of that size, consider a container with a lookup index, such as
69+
[`tsl::ordered_map`](https://github.com/Tessil/ordered-map)
70+
([integration](https://github.com/nlohmann/json/issues/546#issuecomment-304447518)), as the object type -- see
71+
[object order](../features/object_order.md).
72+
3173
## Member types
3274
3375
- **key_type** - key type (`Key`)

docs/mkdocs/docs/features/object_order.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,12 @@ If you do want to preserve the **insertion order**, you can use the type [`nlohm
5353

5454
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)).
5555

56+
The [`ordered_map`](../api/ordered_map.md) behind `nlohmann::ordered_json` is deliberately minimal and has no lookup
57+
index, so every key access is a linear scan and building an object of `n` keys costs O(n²). This is unnoticeable at
58+
typical object sizes but becomes significant for objects with many thousands of keys; see
59+
[`ordered_map` complexity](../api/ordered_map.md#complexity). The alternatives above keep a lookup index and do not
60+
have this cost.
61+
5662
### Notes on parsing
5763

5864
Note that you also need to call the right [`parse`](../api/basic_json/parse.md) function when reading from a file.

0 commit comments

Comments
 (0)