Skip to content

Conversation

puffetto
Copy link
Contributor

@puffetto puffetto commented Oct 5, 2025

This is a re-implementation of ordered_map.

Besides adding a couple unit-tests the only changed "production" file is ordered_map.hpp
The new implementation uses an std::list to store the pair<key,value> entries, plus a std::unordered_map<Key*, list::iterator> for lookup.

  • Changes in detail

WHAT:
The new implementation is not a subclass of vector<>neither of map<>, thus does not "automatically" expose all methods supported by them; however the following have been implemented:

  • All methods of map<> except those strictly related to ordering and those introduced after C++11; this is all the main library depends on (Also reverse iterators and other stuff missing in the previous ordered_map<>).
  • Some methods actually unused by the library but used by the old unit tests have been implemented aswell (iterator+int), here not taking care of performance for obvious reasons

WHY:
See considerations and performance tests below *.

  • If applicable, an existing issue is referenced.

All this starts from #3469; I proposed an approach in which we have a "json::pointer" subclass that can be used as an iterator to walk through the tree; I have the code almost ready but this code relies on three things to be "efficient":
a. json::pointer syntax/manipulation (PR #4860)
b. json::pointer object ( a very simple PR, coming soon, which simply makes json pointers internals to use a list instead of a vector, that's a four-line patch).
c. object access by "key" (This PR)

I decided to propose four separate PRs, one for each of the above and one for the "three walking pointer/itertor"; to make things clear none of these four PRs "depends" on any of the other 3 to work, it's just a matter of performance/optimization: using the forecoming tree walker without these changes can cause severe performance issues.

  • Code coverage (https://coveralls.io/github/nlohmann/json) remained at 100%. A test case for every new line of code.
    I added a unit-ordered-map-2.cpp which SHOULD cover all the code, coverall will tell, in case it's needed I'll act properly. For the sake of safety I left the previous coverage test untouched.

  • If applicable, the documentation is updated.

As it's all "internal" this is probably not needed; main class has doxygen-style comments; if anything else is needed let me know.

  • The source code is amalgamated by running make amalgamate.

Sure.

*) Performance.

I have also added a quick unit-ordered-json-performance.cpp unit; needless to say the new implementation wins hands down on "big" objects (from hundreds of members above), the change for 64K keys in an object is orders of magnitude without noticeable impact on memory usage.

For "small" objects there is a marginal memory impact (in my calculations about 8 pointers per key in total) with an advantage in reduced memory fragmentation (appending to the tail of a vector is the root of all memory storms in C++...).

See below some comparison between "old" and "new" implementation.

Point is that there are two practical uses of this library in my experience: "document like" collections and "data"; for "document like" stuff this implementation does not change much (we are in a range where it is really hard to make performance comparisons); for "data" this makes a change, even just the current unit-unicode* unit tests would take forever with the old ordered_map stuff.

Using "inefficient" approaches (in terms of complexity) behind the scenes exposes to serious risks every time you expose a new feature; say the "three walker" thing gets in and one thinks <<Nice, let me iterate over this ten-thousand {"uuid": {object}, ...}>> and set X to Y in every object... if that's ordered_json with the current implementation the time complexity explodes.

Once again: The last PR I will propose for the three like iterator does not "depend" formally in any way from PR3469, from this one or the one I am about to post for json_iterator as std::list; nor any of these 3 depends on the othe two; it's just that I would not use the tree iterator withOUT the other 3 optimizations.

/// OLD implementation C++11
blackye@undici:~/tmp/json export NLOHMANN_OM_BENCH_N=32768 ; ./build/tests/test-ordered-json-performance_cpp11 --no-skip
[doctest] doctest version is "2.4.12"
[doctest] run with "--help" for options
[ordered_map perf] N=32768  seed=869193496018642825
a) generated keys & shuffles | wall: 7.111      ms | cpu: 7.111      ms | RSS: 4.38 MiB
b) filled object             | wall: 1058.353   ms | cpu: 1058.340   ms | RSS: 8.45 MiB
    sum=536854528
c) random access             | wall: 1026.345   ms | cpu: 1026.329   ms | RSS: 8.45 MiB
d) random erase              | wall: 7102.973   ms | cpu: 7102.946   ms | RSS: 8.47 MiB
===============================================================================
[doctest] test cases: 1 | 1 passed | 0 failed | 0 skipped
[doctest] assertions: 3 | 3 passed | 0 failed |
[doctest] Status: SUCCESS!
blackye@undici:~/tmp/json export NLOHMANN_OM_BENCH_N=65536 ; ./build/tests/test-ordered-json-performance_cpp11 --no-skip
[doctest] doctest version is "2.4.12"
[doctest] run with "--help" for options
[ordered_map perf] N=65536  seed=869193496018642825
a) generated keys & shuffles | wall: 13.227     ms | cpu: 13.227     ms | RSS: 7.38 MiB
b) filled object             | wall: 4180.156   ms | cpu: 4180.050   ms | RSS: 15.47 MiB
    sum=2147450880
c) random access             | wall: 4119.692   ms | cpu: 4119.667   ms | RSS: 15.47 MiB
d) random erase              | wall: 32022.963  ms | cpu: 32022.774  ms | RSS: 15.50 MiB
===============================================================================
[doctest] test cases: 1 | 1 passed | 0 failed | 0 skipped
[doctest] assertions: 3 | 3 passed | 0 failed |
[doctest] Status: SUCCESS!
blackye@undici:~/tmp/json export NLOHMANN_OM_BENCH_N=130072 ; ./build/tests/test-ordered-json-performance_cpp11 --no-skip
[doctest] doctest version is "2.4.12"
[doctest] run with "--help" for options
[ordered_map perf] N=130072  seed=869193496018642825
a) generated keys & shuffles | wall: 24.858     ms | cpu: 24.859     ms | RSS: 13.31 MiB
b) filled object             | wall: 16857.965  ms | cpu: 16852.024  ms | RSS: 30.03 MiB
    sum=8459297556
c) random access             | wall: 16787.402  ms | cpu: 16787.260  ms | RSS: 30.03 MiB
d) random erase              | wall: 119044.936 ms | cpu: 118960.874 ms | RSS: 34.34 MiB
===============================================================================
[doctest] test cases: 1 | 1 passed | 0 failed | 0 skipped
[doctest] assertions: 3 | 3 passed | 0 failed |
[doctest] Status: SUCCESS!
blackye@undici:~/tmp/json export NLOHMANN_OM_BENCH_N=32 ; ./build/tests/test-ordered-json-performance_cpp11 --no-skip
[doctest] doctest version is "2.4.12"
[doctest] run with "--help" for options
[ordered_map perf] N=32  seed=869193496018642825
a) generated keys & shuffles | wall: 0.034      ms | cpu: 0.034      ms | RSS: 1.53 MiB
b) filled object             | wall: 0.035      ms | cpu: 0.035      ms | RSS: 1.55 MiB
    sum=496
c) random access             | wall: 0.021      ms | cpu: 0.020      ms | RSS: 1.55 MiB
d) random erase              | wall: 0.030      ms | cpu: 0.030      ms | RSS: 1.55 MiB
===============================================================================
[doctest] test cases: 1 | 1 passed | 0 failed | 0 skipped
[doctest] assertions: 3 | 3 passed | 0 failed |
[doctest] Status: SUCCESS!
blackye@undici:~/tmp/json export NLOHMANN_OM_BENCH_N=16 ; ./build/tests/test-ordered-json-performance_cpp11 --no-skip
[doctest] doctest version is "2.4.12"
[doctest] run with "--help" for options
[ordered_map perf] N=16  seed=869193496018642825
a) generated keys & shuffles | wall: 0.013      ms | cpu: 0.012      ms | RSS: 1.38 MiB
b) filled object             | wall: 0.028      ms | cpu: 0.029      ms | RSS: 1.39 MiB
    sum=120
c) random access             | wall: 0.016      ms | cpu: 0.016      ms | RSS: 1.39 MiB
d) random erase              | wall: 0.015      ms | cpu: 0.014      ms | RSS: 1.39 MiB
===============================================================================
[doctest] test cases: 1 | 1 passed | 0 failed | 0 skipped
[doctest] assertions: 3 | 3 passed | 0 failed |
[doctest] Status: SUCCESS!
blackye@undici:~/tmp/json 

/// ... and using C++23 (OLD implementation)
test 57
    Start 57: test-ordered-json-performance_cpp11

57: Test command: /Users/blackye/tmp/json/build-cxx23/tests/test-ordered-json-performance_cpp11 "--no-skip"
57: Working Directory: /Users/blackye/tmp/json
57: Test timeout computed to be: 1500
57: [doctest] doctest version is "2.4.12"
57: [doctest] run with "--help" for options
57: [ordered_map perf] N=4  seed=869193496018642825
57: a) generated keys & shuffles | wall: 0.003      ms | cpu: 0.003      ms | RSS: 1.38 MiB
57: b) filled object             | wall: 0.007      ms | cpu: 0.007      ms | RSS: 1.39 MiB
57:     sum=6
57: c) random access             | wall: 0.003      ms | cpu: 0.003      ms | RSS: 1.39 MiB
57: d) random erase              | wall: 0.003      ms | cpu: 0.003      ms | RSS: 1.39 MiB
57: ===============================================================================
57: [doctest] test cases: 1 | 1 passed | 0 failed | 0 skipped
57: [doctest] assertions: 3 | 3 passed | 0 failed |
57: [doctest] Status: SUCCESS!
2/2 Test #57: test-ordered-json-performance_cpp11 ...   Passed    0.00 sec

The following tests passed:
	download_test_data
	test-ordered-json-performance_cpp11

100% tests passed, 0 tests failed out of 2

Label Time Summary:
all    =   0.00 sec*proc (1 test)

Total Test time (real) =   0.12 sec

//---
test 57
    Start 57: test-ordered-json-performance_cpp11

57: Test command: /Users/blackye/tmp/json/build-cxx23/tests/test-ordered-json-performance_cpp11 "--no-skip"
57: Working Directory: /Users/blackye/tmp/json
57: Test timeout computed to be: 1500
57: [doctest] doctest version is "2.4.12"
57: [doctest] run with "--help" for options
57: [ordered_map perf] N=130072  seed=869193496018642825
57: a) generated keys & shuffles | wall: 9.284      ms | cpu: 9.279      ms | RSS: 13.31 MiB
57: b) filled object             | wall: 16700.414  ms | cpu: 16700.232  ms | RSS: 31.12 MiB
57:     sum=8459297556
57: c) random access             | wall: 16784.313  ms | cpu: 16784.221  ms | RSS: 31.12 MiB
57: d) random erase              | wall: 120201.214 ms | cpu: 120053.619 ms | RSS: 34.44 MiB
57: ===============================================================================
57: [doctest] test cases: 1 | 1 passed | 0 failed | 0 skipped
57: [doctest] assertions: 3 | 3 passed | 0 failed |
57: [doctest] Status: SUCCESS!
2/2 Test #57: test-ordered-json-performance_cpp11 ...   Passed  153.70 sec

The following tests passed:
	download_test_data
	test-ordered-json-performance_cpp11

100% tests passed, 0 tests failed out of 2

Label Time Summary:
all    = 153.70 sec*proc (1 test)

Total Test time (real) = 153.83 sec

// ************************* //

/// NEW implementation C++11
blackye@undici:~/json export NLOHMANN_OM_BENCH_N=32768 ; ./build/tests/test-ordered-json-performance_cpp11 --no-skip
[doctest] doctest version is "2.4.12"
[doctest] run with "--help" for options
[ordered_map perf] N=32768  seed=869193496018642825
a) generated keys & shuffles | wall: 7.543      ms | cpu: 7.508      ms | RSS: 4.45 MiB
b) filled object             | wall: 7.107      ms | cpu: 7.049      ms | RSS: 10.09 MiB
    sum=536854528
c) random access             | wall: 4.663      ms | cpu: 4.664      ms | RSS: 10.09 MiB
d) random erase              | wall: 7.795      ms | cpu: 7.738      ms | RSS: 10.09 MiB
===============================================================================
[doctest] test cases: 1 | 1 passed | 0 failed | 0 skipped
[doctest] assertions: 3 | 3 passed | 0 failed |
[doctest] Status: SUCCESS!
blackye@undici:~/json export NLOHMANN_OM_BENCH_N=65536 ; ./build/tests/test-ordered-json-performance_cpp11 --no-skip
[doctest] doctest version is "2.4.12"
[doctest] run with "--help" for options
[ordered_map perf] N=65536  seed=869193496018642825
a) generated keys & shuffles | wall: 13.854     ms | cpu: 13.843     ms | RSS: 7.48 MiB
b) filled object             | wall: 25.389     ms | cpu: 25.279     ms | RSS: 18.33 MiB
    sum=2147450880
c) random access             | wall: 22.872     ms | cpu: 22.834     ms | RSS: 18.34 MiB
d) random erase              | wall: 24.552     ms | cpu: 24.528     ms | RSS: 18.34 MiB
===============================================================================
[doctest] test cases: 1 | 1 passed | 0 failed | 0 skipped
[doctest] assertions: 3 | 3 passed | 0 failed |
[doctest] Status: SUCCESS!
blackye@undici:~/json export NLOHMANN_OM_BENCH_N=130072 ; ./build/tests/test-ordered-json-performance_cpp11 --no-skip
[doctest] doctest version is "2.4.12"
[doctest] run with "--help" for options
[ordered_map perf] N=130072  seed=869193496018642825
a) generated keys & shuffles | wall: 24.475     ms | cpu: 24.475     ms | RSS: 13.44 MiB
b) filled object             | wall: 50.777     ms | cpu: 50.777     ms | RSS: 34.55 MiB
    sum=8459297556
c) random access             | wall: 36.487     ms | cpu: 36.477     ms | RSS: 34.55 MiB
d) random erase              | wall: 50.337     ms | cpu: 50.332     ms | RSS: 34.56 MiB
===============================================================================
[doctest] test cases: 1 | 1 passed | 0 failed | 0 skipped
[doctest] assertions: 3 | 3 passed | 0 failed |
[doctest] Status: SUCCESS!
blackye@undici:~/json export NLOHMANN_OM_BENCH_N=32 ; ./build/tests/test-ordered-json-performance_cpp11 --no-skip
[doctest] doctest version is "2.4.12"
[doctest] run with "--help" for options
[ordered_map perf] N=32  seed=869193496018642825
a) generated keys & shuffles | wall: 0.024      ms | cpu: 0.024      ms | RSS: 1.39 MiB
b) filled object             | wall: 0.048      ms | cpu: 0.048      ms | RSS: 1.41 MiB
    sum=496
c) random access             | wall: 0.018      ms | cpu: 0.017      ms | RSS: 1.41 MiB
d) random erase              | wall: 0.015      ms | cpu: 0.015      ms | RSS: 1.41 MiB
===============================================================================
[doctest] test cases: 1 | 1 passed | 0 failed | 0 skipped
[doctest] assertions: 3 | 3 passed | 0 failed |
[doctest] Status: SUCCESS!
blackye@undici:~/json export NLOHMANN_OM_BENCH_N=16 ; ./build/tests/test-ordered-json-performance_cpp11 --no-skip
[doctest] doctest version is "2.4.12"
[doctest] run with "--help" for options
[ordered_map perf] N=16  seed=869193496018642825
a) generated keys & shuffles | wall: 0.053      ms | cpu: 0.053      ms | RSS: 1.53 MiB
b) filled object             | wall: 0.048      ms | cpu: 0.047      ms | RSS: 1.56 MiB
    sum=120
c) random access             | wall: 0.020      ms | cpu: 0.020      ms | RSS: 1.56 MiB
d) random erase              | wall: 0.016      ms | cpu: 0.017      ms | RSS: 1.56 MiB
===============================================================================
[doctest] test cases: 1 | 1 passed | 0 failed | 0 skipped
[doctest] assertions: 3 | 3 passed | 0 failed |
[doctest] Status: SUCCESS!
blackye@undici:~/json 

/// ... and using C++23 (NEW implementation)
test 57
    Start 57: test-ordered-json-performance_cpp11

57: Test command: /Users/blackye/json/build-cxx23/tests/test-ordered-json-performance_cpp11 "--no-skip"
57: Working Directory: /Users/blackye/json
57: Test timeout computed to be: 1500
57: [doctest] doctest version is "2.4.12"
57: [doctest] run with "--help" for options
57: [ordered_map perf] N=4  seed=869193496018642825
57: a) generated keys & shuffles | wall: 0.003      ms | cpu: 0.003      ms | RSS: 1.38 MiB
57: b) filled object             | wall: 0.006      ms | cpu: 0.006      ms | RSS: 1.38 MiB
57:     sum=6
57: c) random access             | wall: 0.003      ms | cpu: 0.003      ms | RSS: 1.38 MiB
57: d) random erase              | wall: 0.003      ms | cpu: 0.002      ms | RSS: 1.38 MiB
57: ===============================================================================
57: [doctest] test cases: 1 | 1 passed | 0 failed | 0 skipped
57: [doctest] assertions: 3 | 3 passed | 0 failed |
57: [doctest] Status: SUCCESS!
2/2 Test #57: test-ordered-json-performance_cpp11 ...   Passed    0.00 sec

The following tests passed:
	download_test_data
	test-ordered-json-performance_cpp11

100% tests passed, 0 tests failed out of 2

Label Time Summary:
all    =   0.00 sec*proc (1 test)

Total Test time (real) =   0.12 sec

//---
test 57
    Start 57: test-ordered-json-performance_cpp11

57: Test command: /Users/blackye/json/build-cxx23/tests/test-ordered-json-performance_cpp11 "--no-skip"
57: Working Directory: /Users/blackye/json
57: Test timeout computed to be: 1500
57: [doctest] doctest version is "2.4.12"
57: [doctest] run with "--help" for options
57: [ordered_map perf] N=130072  seed=869193496018642825
57: a) generated keys & shuffles | wall: 9.347      ms | cpu: 9.342      ms | RSS: 13.42 MiB
57: b) filled object             | wall: 28.416     ms | cpu: 28.417     ms | RSS: 34.55 MiB
57:     sum=8459297556
57: c) random access             | wall: 34.393     ms | cpu: 34.389     ms | RSS: 34.55 MiB
57: d) random erase              | wall: 46.727     ms | cpu: 46.723     ms | RSS: 34.55 MiB
57: ===============================================================================
57: [doctest] test cases: 1 | 1 passed | 0 failed | 0 skipped
57: [doctest] assertions: 3 | 3 passed | 0 failed |
57: [doctest] Status: SUCCESS!
2/2 Test #57: test-ordered-json-performance_cpp11 ...   Passed    0.12 sec

The following tests passed:
	download_test_data
	test-ordered-json-performance_cpp11

100% tests passed, 0 tests failed out of 2

Label Time Summary:
all    =   0.12 sec*proc (1 test)

Total Test time (real) =   0.24 sec


@coveralls
Copy link

Coverage Status

coverage: 98.923% (-0.3%) from 99.191%
when pulling 1a16b3a on puffetto:feature/ordered-map
into 3ed64e5 on nlohmann:develop.

@gregmarr
Copy link
Contributor

gregmarr commented Oct 6, 2025

Looks like you are only benchmarking random access, and not sequential access, parsing from JSON, or dumping to JSON. I would expect the current vector to have much better performance than a list for these use cases, as vector generally outperforms list by a large amount for sequential access when the containers are large. Have you also benchmarked memory usage of the containers?

Can you summarize all the results in a single table rather than having them spread all over the description?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants