diff --git a/.github/dependabot.yml b/.github/dependabot.yml index aec207e..f87be70 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,7 +1,16 @@ version: 2 updates: - package-ecosystem: nuget - directory: "/" + directory: "/csharp" + schedule: + interval: daily + open-pull-requests-limit: 10 +- package-ecosystem: cargo + directory: "/rust" schedule: interval: daily open-pull-requests-limit: 10 +- package-ecosystem: github-actions + directory: "/" + schedule: + interval: weekly diff --git a/.github/workflows/AutoMerge.yml b/.github/workflows/AutoMerge.yml index d5c5e0c..7b8c981 100644 --- a/.github/workflows/AutoMerge.yml +++ b/.github/workflows/AutoMerge.yml @@ -7,7 +7,7 @@ jobs: auto-merge: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v7 - uses: ahmadnassri/action-dependabot-auto-merge@v2 with: target: minor diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml deleted file mode 100644 index a3c0931..0000000 --- a/.github/workflows/CI.yml +++ /dev/null @@ -1,198 +0,0 @@ -name: CI - -on: - push: - branches: [main] - paths: - - 'csharp/**' - - 'scripts/**' - - '.github/workflows/CI.yml' - pull_request: - branches: [main] - paths: - - 'csharp/**' - - 'scripts/**' - - '.github/workflows/CI.yml' - -defaults: - run: - working-directory: csharp - -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: ${{ github.event_name == 'pull_request' }} - -jobs: - test: - name: Test (${{ matrix.os }}) - runs-on: ${{ matrix.os }} - timeout-minutes: 30 - strategy: - fail-fast: false - matrix: - os: [ubuntu-latest, macos-latest, windows-latest] - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-dotnet@v4 - with: - dotnet-version: '8.0.x' - - name: Build - run: dotnet build Comparisons.SQLiteVSDoublets.sln -c Release --warnaserror - - name: Verify every storage variant - run: dotnet run -c Release --no-build -- --self-test - - results-pipeline: - name: Results pipeline tests - runs-on: ubuntu-latest - timeout-minutes: 10 - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: '3.11' - - name: Install chart dependencies - run: python -m pip install matplotlib numpy - working-directory: . - - name: Test shared and C# reporting - run: | - python -m unittest discover -s scripts -p 'test_*.py' -v - python -m unittest discover -s csharp -p 'test_*.py' -v - working-directory: . - - benchmark-pr: - name: Benchmark (PR validation) - if: github.event_name == 'pull_request' - needs: [test, results-pipeline] - runs-on: ubuntu-latest - timeout-minutes: 45 - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-dotnet@v4 - with: - dotnet-version: '8.0.x' - - uses: actions/setup-python@v5 - with: - python-version: '3.11' - - name: Install chart dependencies - run: python -m pip install matplotlib numpy - working-directory: . - - name: Build benchmark - run: dotnet build Comparisons.SQLiteVSDoublets.sln -c Release - - name: Run reduced-scale benchmark - env: - BENCHMARK_LINK_COUNT: 10 - BACKGROUND_LINK_COUNT: 30 - BENCHMARK_WARMUP_COUNT: 1 - BENCHMARK_ITERATION_COUNT: 1 - run: >- - dotnet run -c Release --no-build -- - --filter '*LinksBenchmarks*' > benchmark.log 2>&1 - - name: Generate table and charts - env: - BENCHMARK_LINK_COUNT: 10 - BACKGROUND_LINK_COUNT: 30 - run: | - mkdir -p artifacts - report_path=$(find BenchmarkDotNet.Artifacts/results -name '*LinksBenchmarks-report-full.json' -print -quit) - test -n "$report_path" - python out.py "$report_path" \ - --results artifacts/results.md \ - --output-dir artifacts \ - --docs-dir artifacts/docs - - name: Add results to job summary - run: | - { - echo '## C# benchmark (reduced-scale validation)' - echo - cat artifacts/results.md - echo - echo '_These numbers validate the pipeline; full results are published from `main`._' - } >> "$GITHUB_STEP_SUMMARY" - - name: Upload diagnostic and report artifacts - if: always() - uses: actions/upload-artifact@v4 - with: - name: csharp-benchmark-pr - path: | - csharp/benchmark.log - csharp/BenchmarkDotNet.Artifacts/results/ - csharp/artifacts/ - - benchmark: - name: Benchmark (full) - if: github.event_name == 'push' && github.ref == 'refs/heads/main' - needs: [test, results-pipeline] - runs-on: ubuntu-latest - timeout-minutes: 180 - concurrency: - group: benchmark-publication - cancel-in-progress: false - permissions: - contents: write - steps: - - uses: actions/checkout@v4 - with: - ref: main - fetch-depth: 0 - - uses: actions/setup-dotnet@v4 - with: - dotnet-version: '8.0.x' - - uses: actions/setup-python@v5 - with: - python-version: '3.11' - - name: Install chart dependencies - run: python -m pip install matplotlib numpy - working-directory: . - - name: Build benchmark - run: dotnet build Comparisons.SQLiteVSDoublets.sln -c Release - - name: Run full benchmark - env: - BENCHMARK_LINK_COUNT: 1000 - BACKGROUND_LINK_COUNT: 3000 - BENCHMARK_WARMUP_COUNT: 2 - BENCHMARK_ITERATION_COUNT: 5 - run: >- - dotnet run -c Release --no-build -- - --filter '*LinksBenchmarks*' > benchmark.log 2>&1 - - name: Generate and publish documentation - env: - BENCHMARK_LINK_COUNT: 1000 - BACKGROUND_LINK_COUNT: 3000 - run: | - report_path=$(find BenchmarkDotNet.Artifacts/results -name '*LinksBenchmarks-report-full.json' -print -quit) - test -n "$report_path" - python out.py "$report_path" \ - --results results.md \ - --readme ../README.md \ - --readme ../README.ru.md \ - --docs-dir ../docs/benchmarks - - name: Add results to job summary - run: | - { - echo '## C# benchmark (full scale)' - echo - cat results.md - } >> "$GITHUB_STEP_SUMMARY" - - name: Commit published results - working-directory: . - run: | - git config user.email 'linksplatform@gmail.com' - git config user.name 'LinksPlatformBencher' - git add README.md README.ru.md csharp/results.md docs/benchmarks/ - if git diff --staged --quiet; then - echo 'No benchmark documentation changed.' - else - git commit -m 'Update C# benchmark results [skip ci]' - git push origin HEAD:main - fi - - name: Upload diagnostic and report artifacts - if: always() - uses: actions/upload-artifact@v4 - with: - name: csharp-benchmark-full - path: | - csharp/benchmark.log - csharp/BenchmarkDotNet.Artifacts/results/ - csharp/results.md - csharp/bench_csharp.png - csharp/bench_csharp_log_scale.png diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml new file mode 100644 index 0000000..1144167 --- /dev/null +++ b/.github/workflows/benchmarks.yml @@ -0,0 +1,136 @@ +name: Benchmarks + +# Every job measures one README table (language, category, bits, size) with all its variants on one runner, +# so that the variants of a table are compared on the same machine. Pull requests run the whole pipeline +# on small sizes; pushes to main publish the results to README.md and README.ru.md. +on: + push: + branches: [main] + paths: ['rust/**', 'csharp/**', 'scripts/**', '.github/workflows/benchmarks.yml'] + pull_request: + paths: ['rust/**', 'csharp/**', 'scripts/**', '.github/workflows/benchmarks.yml'] + workflow_dispatch: + inputs: + sizes: + description: JSON array of sizes (objects skip 10,000,000 and 100,000,000) + default: '[100000, 1000000, 10000000]' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +env: + CARGO_TERM_COLOR: always + +jobs: + report-tests: + name: Report script tests + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-python@v7 + with: + python-version: '3.13' + - run: pip install -r scripts/requirements.txt + - run: python -m unittest discover -s scripts -v + - run: npx --yes markdownlint-cli@0.49.1 README.md README.ru.md + + benchmark: + name: ${{ matrix.language }} ${{ matrix.category }} ${{ matrix.bits }} bit ${{ matrix.size }} + runs-on: ubuntu-latest + timeout-minutes: 355 + strategy: + fail-fast: false + matrix: + language: [rust, csharp] + category: [links, objects] + bits: [32, 64] + size: ${{ fromJSON(inputs.sizes || (github.event_name == 'pull_request' && '[1000]') || '[100000, 1000000, 10000000]') }} + exclude: + # One million blog posts already take an hour in C#; ten millions would not fit into a job. + - category: objects + size: 10000000 + - category: objects + size: 100000000 + steps: + - uses: actions/checkout@v7 + - if: matrix.language == 'rust' + uses: dtolnay/rust-toolchain@stable + - if: matrix.language == 'rust' + uses: Swatinem/rust-cache@v2 + with: + workspaces: rust + - if: matrix.language == 'rust' + run: | + cargo build --release --manifest-path rust/Cargo.toml + echo "BENCHMARK=rust/target/release/sqlite-vs-doublets" >> "$GITHUB_ENV" + - if: matrix.language == 'csharp' + uses: actions/setup-dotnet@v6 + with: + global-json-file: csharp/global.json + - if: matrix.language == 'csharp' + run: | + dotnet build -c Release csharp/SQLiteVSDoublets + echo "BENCHMARK=dotnet csharp/SQLiteVSDoublets/bin/Release/net10.0/sqlite-vs-doublets.dll" >> "$GITHUB_ENV" + - name: Benchmark + run: | + name=${{ matrix.category }}-${{ matrix.language }}-${{ matrix.bits }}-${{ matrix.size }} + echo "NAME=$name" >> "$GITHUB_ENV" + $BENCHMARK ${{ matrix.category }} ${{ matrix.bits }} ${{ matrix.size }} \ + --directory "$RUNNER_TEMP/storage" --output "$RUNNER_TEMP/measured.json" + machine="$(. /etc/os-release && echo "$PRETTY_NAME"), $(lscpu | sed -n 's/^Model name: *//p' | head -1)" + machine="$machine, $(nproc) cores, $(awk '/^MemTotal/ { printf "%.0f", $2 / 1048576 }' /proc/meminfo) GiB" + mkdir results + jq --arg run_url "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" \ + --arg date "$(date -u +%Y-%m-%d)" --arg machine "$machine" \ + '. + {run_url: $run_url, date: $date, machine: $machine}' \ + "$RUNNER_TEMP/measured.json" > "results/$name.json" + - uses: actions/upload-artifact@v7 + with: + name: results-${{ env.NAME }} + path: results/${{ env.NAME }}.json + + report: + name: Report + needs: [report-tests, benchmark] + # Publishes the finished tables even if some jobs failed; the missing ones keep their previous results. + if: ${{ !cancelled() && needs.report-tests.result == 'success' }} + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + contents: write + steps: + - uses: actions/checkout@v7 + with: + ref: ${{ github.event_name == 'pull_request' && github.sha || github.ref_name }} + - uses: actions/setup-python@v7 + with: + python-version: '3.13' + - run: pip install -r scripts/requirements.txt + - uses: actions/download-artifact@v8 + with: + pattern: results-* + merge-multiple: true + path: ${{ github.event_name == 'pull_request' && format('{0}/results', runner.temp) || 'docs/benchmarks/results' }} + - if: github.event_name == 'pull_request' + name: Check the report + run: | + python scripts/benchmark_report.py "$RUNNER_TEMP/results" >> "$GITHUB_STEP_SUMMARY" + python scripts/benchmark_report.py "$RUNNER_TEMP/results" --readme README.md --readme README.ru.md + npx --yes markdownlint-cli@0.49.1 README.md README.ru.md + - if: github.event_name != 'pull_request' + name: Publish the report + run: | + python scripts/benchmark_report.py docs/benchmarks/results \ + --readme README.md --readme README.ru.md --charts docs/benchmarks + git config user.email 'linksplatform@gmail.com' + git config user.name 'LinksPlatformBencher' + git add README.md README.ru.md docs/benchmarks + git diff --staged --quiet && exit 0 + git commit -m 'Update benchmark results [skip ci]' + for attempt in 1 2 3; do + git pull --rebase origin "$GITHUB_REF_NAME" && git push origin "HEAD:$GITHUB_REF_NAME" && exit 0 + sleep 10 + done + exit 1 diff --git a/.github/workflows/cpp.yml b/.github/workflows/cpp.yml index f9ddc21..9f8ca4a 100644 --- a/.github/workflows/cpp.yml +++ b/.github/workflows/cpp.yml @@ -20,7 +20,7 @@ jobs: test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v7 with: submodules: true - name: Setup clang @@ -59,7 +59,7 @@ jobs: runs-on: ubuntu-latest needs: test steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v7 with: submodules: true - name: Read package information @@ -76,7 +76,7 @@ jobs: runs-on: ubuntu-latest needs: test steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v7 with: submodules: true - name: Read project information diff --git a/.github/workflows/csharp.yml b/.github/workflows/csharp.yml new file mode 100644 index 0000000..cf693ae --- /dev/null +++ b/.github/workflows/csharp.yml @@ -0,0 +1,33 @@ +name: C# + +on: + push: + branches: [main] + paths: ['csharp/**', '.github/workflows/csharp.yml'] + pull_request: + paths: ['csharp/**', '.github/workflows/csharp.yml'] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +defaults: + run: + working-directory: csharp + +jobs: + test: + name: Test (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-dotnet@v6 + with: + global-json-file: csharp/global.json + - run: dotnet build -c Release -warnaserror + - run: dotnet test --project SQLiteVSDoublets.Tests -c Release --no-build diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 0d5d0bb..2123a32 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -3,232 +3,38 @@ name: Rust on: push: branches: [main] - paths: - - 'rust/**' - - 'scripts/**' - - '.github/workflows/rust.yml' + paths: ['rust/**', '.github/workflows/rust.yml'] pull_request: - branches: [main] - paths: - - 'rust/**' - - 'scripts/**' - - '.github/workflows/rust.yml' + paths: ['rust/**', '.github/workflows/rust.yml'] -env: - CARGO_TERM_COLOR: always - RUST_BACKTRACE: 1 - RUST_TOOLCHAIN: nightly-2022-08-22 +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true defaults: run: working-directory: rust -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: ${{ github.event_name == 'pull_request' }} +env: + CARGO_TERM_COLOR: always jobs: test: name: Test (${{ matrix.os }}) runs-on: ${{ matrix.os }} - timeout-minutes: 45 + timeout-minutes: 30 strategy: fail-fast: false matrix: os: [ubuntu-latest, macos-latest, windows-latest] steps: - - uses: actions/checkout@v4 - - - name: Setup pinned Rust nightly - uses: dtolnay/rust-toolchain@master + - uses: actions/checkout@v7 + - uses: dtolnay/rust-toolchain@stable with: - toolchain: ${{ env.RUST_TOOLCHAIN }} components: rustfmt, clippy - - - name: Cache Rust dependencies - uses: Swatinem/rust-cache@v2 - with: - workspaces: rust -> target - cache-on-failure: 'true' - - - name: Check formatting - run: cargo fmt --all -- --check - - - name: Run Clippy - run: cargo clippy --lib --tests --benches -- -D warnings - - - name: Run unit and integration tests - run: cargo test --release --lib --tests - - # `cargo test --all-targets` executes Criterion binaries. Compile them - # explicitly so file-backed measurements are only run in bounded jobs. - - name: Compile benchmarks - run: cargo check --release --benches - - results-pipeline: - name: Results pipeline tests - runs-on: ubuntu-latest - timeout-minutes: 10 - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: '3.11' - - name: Install chart dependencies - run: python -m pip install matplotlib numpy - working-directory: . - - name: Test shared and Rust reporting - run: | - python -m unittest discover -s scripts -p 'test_*.py' -v - python -m unittest discover -s rust -p 'test_*.py' -v - working-directory: . - - benchmark-pr: - name: Benchmark (PR validation) - if: github.event_name == 'pull_request' - needs: [test, results-pipeline] - runs-on: ubuntu-latest - timeout-minutes: 45 - steps: - - uses: actions/checkout@v4 - - name: Setup pinned Rust nightly - uses: dtolnay/rust-toolchain@master - with: - toolchain: ${{ env.RUST_TOOLCHAIN }} - components: rustfmt, clippy - - uses: actions/setup-python@v5 - with: - python-version: '3.11' - - name: Install chart dependencies - run: python -m pip install matplotlib numpy - working-directory: . - - name: Cache Rust dependencies - uses: Swatinem/rust-cache@v2 - with: - workspaces: rust -> target - cache-on-failure: 'true' - - name: Run reduced-scale benchmark - env: - BENCHMARK_LINK_COUNT: 10 - BACKGROUND_LINK_COUNT: 30 - BENCHMARK_OBJECT_COUNT: 5 - BENCHMARK_SAMPLE_SIZE: 10 - BENCHMARK_MEASUREMENT_SECONDS: 1 - BENCHMARK_WARM_UP_MILLISECONDS: 100 - run: | - set -o pipefail - rm -rf target/criterion - cargo bench --bench bench -- --output-format bencher | tee out.txt - - name: Generate table and charts - env: - BENCHMARK_LINK_COUNT: 10 - BACKGROUND_LINK_COUNT: 30 - BENCHMARK_OBJECT_COUNT: 5 - run: | - mkdir -p artifacts - python out.py out.txt \ - --results artifacts/results.md \ - --output-dir artifacts \ - --docs-dir artifacts/docs - - name: Add results to job summary - run: | - { - echo '## Rust benchmark (reduced-scale validation)' - echo - cat artifacts/results.md - echo - echo '_These numbers validate the pipeline; full results are published from `main`._' - } >> "$GITHUB_STEP_SUMMARY" - - name: Upload diagnostic and report artifacts - if: always() - uses: actions/upload-artifact@v4 - with: - name: rust-benchmark-pr - path: | - rust/out.txt - rust/artifacts/ - - benchmark: - name: Benchmark (full) - if: github.event_name == 'push' && github.ref == 'refs/heads/main' - needs: [test, results-pipeline] - runs-on: ubuntu-latest - timeout-minutes: 180 - concurrency: - group: benchmark-publication - cancel-in-progress: false - permissions: - contents: write - steps: - - uses: actions/checkout@v4 - with: - ref: main - fetch-depth: 0 - - name: Setup pinned Rust nightly - uses: dtolnay/rust-toolchain@master - with: - toolchain: ${{ env.RUST_TOOLCHAIN }} - components: rustfmt, clippy - - uses: actions/setup-python@v5 - with: - python-version: '3.11' - - name: Install chart dependencies - run: python -m pip install matplotlib numpy - working-directory: . - - name: Cache Rust dependencies - uses: Swatinem/rust-cache@v2 - with: - workspaces: rust -> target - cache-on-failure: 'true' - - name: Run full benchmark - env: - BENCHMARK_LINK_COUNT: 1000 - BACKGROUND_LINK_COUNT: 3000 - BENCHMARK_OBJECT_COUNT: 1000 - BENCHMARK_SAMPLE_SIZE: 20 - BENCHMARK_MEASUREMENT_SECONDS: 3 - BENCHMARK_WARM_UP_MILLISECONDS: 1000 - run: | - set -o pipefail - rm -rf target/criterion - cargo bench --bench bench -- --output-format bencher | tee out.txt - - name: Generate and publish documentation - env: - BENCHMARK_LINK_COUNT: 1000 - BACKGROUND_LINK_COUNT: 3000 - BENCHMARK_OBJECT_COUNT: 1000 - run: | - python out.py out.txt \ - --results results.md \ - --readme ../README.md \ - --readme ../README.ru.md \ - --docs-dir ../docs/benchmarks - - name: Add results to job summary - run: | - { - echo '## Rust benchmark (full scale)' - echo - cat results.md - } >> "$GITHUB_STEP_SUMMARY" - - name: Commit published results - working-directory: . - run: | - git config user.email 'linksplatform@gmail.com' - git config user.name 'LinksPlatformBencher' - git add README.md README.ru.md rust/results.md docs/benchmarks/ - if git diff --staged --quiet; then - echo 'No benchmark documentation changed.' - else - git commit -m 'Update Rust benchmark results [skip ci]' - git push origin HEAD:main - fi - - name: Upload diagnostic and report artifacts - if: always() - uses: actions/upload-artifact@v4 + - uses: Swatinem/rust-cache@v2 with: - name: rust-benchmark-full - path: | - rust/out.txt - rust/results.md - rust/bench_rust.png - rust/bench_rust_log_scale.png + workspaces: rust + - run: cargo fmt --check + - run: cargo clippy --release --all-targets -- -D warnings + - run: cargo test --release diff --git a/.gitignore b/.gitignore index 6377d22..e5c9ab8 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,10 @@ __pycache__/ *.swp *~ .DS_Store + +# Rust build output +target/ + +# .NET build output +bin/ +obj/ diff --git a/README.md b/README.md index b5b2924..9530dbe 100644 --- a/README.md +++ b/README.md @@ -1,91 +1,462 @@ -[![Actions Status](https://github.com/linksplatform/Comparisons.SQLiteVSDoublets/workflows/CI/badge.svg)](https://github.com/linksplatform/Comparisons.SQLiteVSDoublets/actions?workflow=CI) - # Comparisons.SQLiteVSDoublets ([русская версия](README.ru.md)) +[![Rust](https://github.com/linksplatform/Comparisons.SQLiteVSDoublets/actions/workflows/rust.yml/badge.svg)](https://github.com/linksplatform/Comparisons.SQLiteVSDoublets/actions/workflows/rust.yml) +[![C#](https://github.com/linksplatform/Comparisons.SQLiteVSDoublets/actions/workflows/csharp.yml/badge.svg)](https://github.com/linksplatform/Comparisons.SQLiteVSDoublets/actions/workflows/csharp.yml) +[![Benchmarks](https://github.com/linksplatform/Comparisons.SQLiteVSDoublets/actions/workflows/benchmarks.yml/badge.svg)](https://github.com/linksplatform/Comparisons.SQLiteVSDoublets/actions/workflows/benchmarks.yml) + ![Comparison of models](https://github.com/LinksPlatform/Documentation/raw/master/doc/ModelsComparison/relational_model_vs_associative_model_vs_links.png) -Comparison of SQLite and LinksPlatform's Doublets (links) on basic embedded database operations with links and object-like structures. +Comparison of SQLite and LinksPlatform's Doublets (links) on basic embedded +database operations with links and object-like structures. + +Based on examples from and + + +## Benchmarks + +Rust ([doublets](https://crates.io/crates/doublets) and +[rusqlite](https://crates.io/crates/rusqlite) with bundled SQLite) and C# +([Platform.Data.Doublets](https://www.nuget.org/packages/Platform.Data.Doublets), +[Platform.Data.Doublets.Sequences](https://www.nuget.org/packages/Platform.Data.Doublets.Sequences) +and [Microsoft.Data.Sqlite](https://www.nuget.org/packages/Microsoft.Data.Sqlite)) +run the same workloads on the same deterministic data, with 32 bit +(`u32`/`uint`) and 64 bit (`u64`/`ulong`) ids: + +- **Links**: SQLite stores the table + `links(id INTEGER PRIMARY KEY, "from", "to")` with the indices + `("from", "to")` and `("to", "from")`; Doublets stores the links themselves. + The operations are create, read all, read by id, search by `(from, to)`, + read by `from`, read by `to`, update (swap `from` and `to`) and delete. +- **Objects**: blog posts with a title, content and publication date. SQLite + stores the table `blog_posts(id, title, content, publication_date)`; + Doublets stores every post as links, with strings as sequences of Unicode + symbols, like + [Platform.Data.Doublets.Sequences](https://github.com/linksplatform/Data.Doublets.Sequences) + does. The operations are create, read all, read by id and delete. Every + Doublets store runs with and without a cache of the string sequences + (`Cached`/`Uncached`). + +Storages: SQLite in memory and in a file; Doublets united (one array of links +with index trees) and split (separate data and index arrays), each volatile +(in memory) and non-volatile (in memory-mapped files). Doublets are compared +with SQLite of the same durability: volatile with `SQLite Memory`, +non-volatile with `SQLite File`. + +Every repetition runs on a fresh store in an empty directory, after discarded +warm-up repetitions on up to 10,000 records that run for at least a second, so +the .NET JIT has already optimized the code. Each operation is one +timed transaction over all records, point operations visit the records in a +scattered order, and each result is checked against the expected count and +order-sensitive checksum, so a storage that loses, duplicates or mixes up +records fails the run instead of being reported as fast. Repetitions are +`3,000,000 / size` for links and `500,000 / size` for objects, clamped to +`1..=10`. The tables show the median time per operation; a difference is +reported only if the interquartile ranges (the middle half) of the +repetitions do not overlap and the medians differ by more than 5%, otherwise +it is `≈ same`. The file size is measured after creation; Doublets files are +preallocated memory-mapped files, so small stores show the preallocation size. + +Every table is measured by its own GitHub Actions job, with all variants on +the same runner, so that a difference is never a difference between machines, +by the [Benchmarks workflow](.github/workflows/benchmarks.yml): links with +100,000, 1,000,000 and 10,000,000 records and objects with 100,000 and +1,000,000 records. These are the largest sizes that fit into the 6 hour limit +of a job: one SQLite File repetition on 100,000,000 links did not finish in +4.5 hours +([run 37228604862](https://github.com/linksplatform/Comparisons.SQLiteVSDoublets/actions/runs/37228604862)), +and 1,000,000 blog posts already take an hour in C#. Pull requests check the +whole pipeline on 1,000 records, and pushes to `main` update the results below. +The workflow can also be started manually with other sizes. + +Run locally: -Based on examples from https://github.com/FahaoTang/dotnetcore-examples and https://github.com/Konard/LinksPlatform +```bash +mkdir -p results +cargo run --release --manifest-path rust/Cargo.toml -- \ + links 64 100000 --output results/links-rust-64-100000.json +dotnet run -c Release --project csharp/SQLiteVSDoublets -- \ + objects 32 1000 --variants SQLite_File,Doublets_Split_NonVolatile_Cached +# print the tables, add --readme README.md --charts docs/benchmarks to update +python3 scripts/benchmark_report.py results +``` -## Automated benchmark suite +Notes: + +- The split stores of doublets 0.5.0 (Rust) lose a link that is updated to + reference itself, so the benchmark updates a link to `(0, 0)` first, which + is the state links are created in + ([experiments/split_store_delete](experiments/split_store_delete)). +- The stores of doublets 0.5.0 (Rust) take the part of the memory that + `platform-mem` 0.3.0 returns after growing it for the whole memory, so a + store fails after 1,040,384 links; the benchmarks wrap the memory in + [`memory::Whole`](rust/src/memory.rs), which returns the whole memory + ([experiments/unit_store_growth](experiments/unit_store_growth)). +- In C#, links are deleted with `Delete(id, handler: null)`, which resets the + link before deleting it; the bare `Delete(id)` leaves the link in the index + trees, and later searches fail + ([experiments/csharp_tree_delete](experiments/csharp_tree_delete)). +- The C# united stores use AVL index trees: the default size balanced trees + degenerate when many links share a source or a target, which makes each + blog post creation linear in the number of posts + ([experiments/csharp_objects_profile](experiments/csharp_objects_profile)). +- The C# split stores keep the default linked list of the internal sources, + which doublets 0.5.0 (Rust) does not have; it makes the C# updates faster + and the other operations take about the same time + ([experiments/csharp_split_linked_list](experiments/csharp_split_linked_list)). +- Objects stores use external references for numbers and Unicode symbols, so + raw values never collide with link ids. +- Reading a string without the cache walks its sequence link by link; in C# + each `GetSource`/`GetTarget` call of `Platform.Data` allocates a handler and + a list, which makes the uncached C# reads much slower than the Rust ones. + + + +## Doublets vs SQLite as storage for links + +### Rust doublets vs SQLite + +#### 32 bit address/id space benchmarks + +##### 100,000 links + +_10 repetitions after a warm-up, median time per operation. SQLite 3.53.2, Ubuntu 24.04.5 LTS, AMD EPYC 9V74 80-Core Processor, 4 cores, 16 GiB, [GitHub Actions run](https://github.com/linksplatform/Comparisons.SQLiteVSDoublets/actions/runs/37226110636) on 2026-10-04._ + +| Storage | Create | Read all | Read by id | Search (from, to) | Read by from | Read by to | Update | Delete | File size | +| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | +| SQLite Memory | 1.72 µs | 125 ns | 606 ns | 693 ns | 765 ns | 766 ns | 3.36 µs | 2.17 µs | — | +| SQLite File | 2.09 µs | 128 ns | 621 ns | 727 ns | 782 ns | 790 ns | 12.3 µs | 6.04 µs | 4.4 MiB | +| Doublets United Volatile | 458 ns (3.76× faster) | 1.91 ns (65.7× faster) | 5.72 ns (106× faster) | 194 ns (3.57× faster) | 248 ns (3.09× faster) | 267 ns (2.86× faster) | 1.24 µs (2.72× faster) | 459 ns (4.72× faster) | — | +| Doublets United NonVolatile | 499 ns (4.19× faster) | 1.91 ns (67.3× faster) | 5.88 ns (106× faster) | 214 ns (3.39× faster) | 278 ns (2.81× faster) | 288 ns (2.75× faster) | 1.29 µs (9.52× faster) | 484 ns (12.5× faster) | 32.0 MiB | +| Doublets Split Volatile | 97.1 ns (17.7× faster) | 3.68 ns (34× faster) | 5.56 ns (109× faster) | 58.7 ns (11.8× faster) | 29.4 ns (26× faster) | 30.1 ns (25.5× faster) | 174 ns (19.3× faster) | 607 ns (3.57× faster) | — | +| Doublets Split NonVolatile | 119 ns (17.6× faster) | 3.57 ns (36× faster) | 7.43 ns (83.6× faster) | 61.8 ns (11.8× faster) | 31.2 ns (25.1× faster) | 32 ns (24.7× faster) | 184 ns (66.8× faster) | 649 ns (9.31× faster) | 40.0 MiB | + +##### 1,000,000 links + +_3 repetitions after a warm-up, median time per operation. SQLite 3.53.2, Ubuntu 24.04.5 LTS, AMD EPYC 7763 64-Core Processor, 4 cores, 16 GiB, [GitHub Actions run](https://github.com/linksplatform/Comparisons.SQLiteVSDoublets/actions/runs/37226110636) on 2026-10-04._ + +| Storage | Create | Read all | Read by id | Search (from, to) | Read by from | Read by to | Update | Delete | File size | +| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | +| SQLite Memory | 2.39 µs | 124 ns | 874 ns | 983 ns | 997 ns | 1 µs | 6.48 µs | 4.26 µs | — | +| SQLite File | 5.76 µs | 128 ns | 1.96 µs | 1.9 µs | 1.92 µs | 1.91 µs | 19 µs | 10.6 µs | 48.2 MiB | +| Doublets United Volatile | 755 ns (3.16× faster) | 1.84 ns (67.5× faster) | 20.7 ns (42.2× faster) | 417 ns (2.36× faster) | 579 ns (1.72× faster) | 620 ns (1.61× faster) | 2.39 µs (2.71× faster) | 828 ns (5.14× faster) | — | +| Doublets United NonVolatile | 868 ns (6.64× faster) | 2.11 ns (60.5× faster) | 22.7 ns (86.3× faster) | 502 ns (3.79× faster) | 681 ns (2.82× faster) | 689 ns (2.77× faster) | 2.62 µs (7.27× faster) | 914 ns (11.6× faster) | 32.0 MiB | +| Doublets Split Volatile | 162 ns (14.7× faster) | 3.87 ns (32.1× faster) | 23 ns (38.1× faster) | 159 ns (6.2× faster) | 116 ns (8.58× faster) | 112 ns (8.96× faster) | 457 ns (14.2× faster) | 1.2 µs (3.55× faster) | — | +| Doublets Split NonVolatile | 174 ns (33.1× faster) | 3.91 ns (32.7× faster) | 24.9 ns (78.6× faster) | 167 ns (11.4× faster) | 129 ns (14.9× faster) | 124 ns (15.4× faster) | 494 ns (38.6× faster) | 1.42 µs (7.5× faster) | 40.0 MiB | + +##### 10,000,000 links + +_1 repetition after a warm-up, median time per operation. SQLite 3.53.2, Ubuntu 24.04.5 LTS, AMD EPYC 7763 64-Core Processor, 4 cores, 16 GiB, [GitHub Actions run](https://github.com/linksplatform/Comparisons.SQLiteVSDoublets/actions/runs/37226110636) on 2026-10-04._ + +| Storage | Create | Read all | Read by id | Search (from, to) | Read by from | Read by to | Update | Delete | File size | +| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | +| SQLite Memory | 3.34 µs | 124 ns | 1.39 µs | 1.47 µs | 1.51 µs | 1.49 µs | 8.53 µs | 5.59 µs | — | +| SQLite File | 9.22 µs | 127 ns | 2.18 µs | 2.57 µs | 2.56 µs | 2.53 µs | 24 µs | 14.5 µs | 501.0 MiB | +| Doublets United Volatile | 2.16 µs (1.55× faster) | 2.26 ns (54.6× faster) | 35 ns (39.7× faster) | 1.26 µs (1.17× faster) | 1.56 µs (≈ same) | 1.61 µs (1.08× slower) | 6.55 µs (1.3× faster) | 2.73 µs (2.05× faster) | — | +| Doublets United NonVolatile | 2.42 µs (3.81× faster) | 2.36 ns (53.5× faster) | 32.6 ns (66.8× faster) | 1.27 µs (2.02× faster) | 1.52 µs (1.68× faster) | 1.56 µs (1.62× faster) | 6.91 µs (3.48× faster) | 2.96 µs (4.89× faster) | 320.0 MiB | +| Doublets Split Volatile | 317 ns (10.5× faster) | 3.88 ns (31.9× faster) | 30.1 ns (46.2× faster) | 240 ns (6.14× faster) | 170 ns (8.84× faster) | 169 ns (8.82× faster) | 752 ns (11.3× faster) | 2.64 µs (2.12× faster) | — | +| Doublets Split NonVolatile | 467 ns (19.8× faster) | 3.94 ns (32.1× faster) | 34.7 ns (62.7× faster) | 251 ns (10.2× faster) | 181 ns (14.1× faster) | 187 ns (13.5× faster) | 781 ns (30.8× faster) | 2.92 µs (4.96× faster) | 400.0 MiB | + +![Rust doublets vs SQLite, 32 bit, links](docs/benchmarks/links-rust-32.png) + +#### 64 bit address/id space benchmarks + +##### 100,000 links + +_10 repetitions after a warm-up, median time per operation. SQLite 3.53.2, Ubuntu 24.04.5 LTS, AMD EPYC 7763 64-Core Processor, 4 cores, 16 GiB, [GitHub Actions run](https://github.com/linksplatform/Comparisons.SQLiteVSDoublets/actions/runs/37226110636) on 2026-10-04._ + +| Storage | Create | Read all | Read by id | Search (from, to) | Read by from | Read by to | Update | Delete | File size | +| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | +| SQLite Memory | 1.69 µs | 126 ns | 616 ns | 721 ns | 752 ns | 743 ns | 3.12 µs | 2.01 µs | — | +| SQLite File | 2.02 µs | 130 ns | 639 ns | 762 ns | 789 ns | 788 ns | 10.4 µs | 5.18 µs | 4.4 MiB | +| Doublets United Volatile | 449 ns (3.77× faster) | 1.81 ns (69.8× faster) | 3.2 ns (192× faster) | 179 ns (4.04× faster) | 234 ns (3.21× faster) | 253 ns (2.94× faster) | 1.13 µs (2.75× faster) | 444 ns (4.54× faster) | — | +| Doublets United NonVolatile | 471 ns (4.29× faster) | 1.89 ns (68.9× faster) | 3.52 ns (182× faster) | 198 ns (3.85× faster) | 255 ns (3.1× faster) | 269 ns (2.93× faster) | 1.19 µs (8.73× faster) | 462 ns (11.2× faster) | 64.0 MiB | +| Doublets Split Volatile | 114 ns (14.8× faster) | 3.77 ns (33.5× faster) | 4.12 ns (149× faster) | 56.1 ns (12.9× faster) | 27.6 ns (27.2× faster) | 29.2 ns (25.4× faster) | 164 ns (19× faster) | 591 ns (3.41× faster) | — | +| Doublets Split NonVolatile | 106 ns (19× faster) | 3.74 ns (34.8× faster) | 3.91 ns (164× faster) | 57 ns (13.4× faster) | 28.7 ns (27.5× faster) | 29.9 ns (26.4× faster) | 171 ns (61× faster) | 609 ns (8.49× faster) | 80.0 MiB | + +##### 1,000,000 links + +_3 repetitions after a warm-up, median time per operation. SQLite 3.53.2, Ubuntu 24.04.5 LTS, AMD EPYC 7763 64-Core Processor, 4 cores, 16 GiB, [GitHub Actions run](https://github.com/linksplatform/Comparisons.SQLiteVSDoublets/actions/runs/37226110636) on 2026-10-04._ + +| Storage | Create | Read all | Read by id | Search (from, to) | Read by from | Read by to | Update | Delete | File size | +| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | +| SQLite Memory | 2.09 µs | 124 ns | 745 ns | 839 ns | 879 ns | 858 ns | 4.77 µs | 3.14 µs | — | +| SQLite File | 5.65 µs | 127 ns | 1.83 µs | 1.82 µs | 1.85 µs | 1.84 µs | 18.4 µs | 10.3 µs | 48.2 MiB | +| Doublets United Volatile | 750 ns (2.79× faster) | 3.05 ns (40.4× faster) | 20.2 ns (36.9× faster) | 385 ns (2.18× faster) | 471 ns (1.87× faster) | 567 ns (1.51× faster) | 2.55 µs (1.87× faster) | 856 ns (3.66× faster) | — | +| Doublets United NonVolatile | 840 ns (6.72× faster) | 3.5 ns (36.2× faster) | 22.7 ns (80.7× faster) | 495 ns (3.67× faster) | 622 ns (2.97× faster) | 642 ns (2.87× faster) | 2.91 µs (6.31× faster) | 1.02 µs (10× faster) | 64.0 MiB | +| Doublets Split Volatile | 184 ns (11.4× faster) | 4.05 ns (30.5× faster) | 21.6 ns (34.5× faster) | 171 ns (4.9× faster) | 130 ns (6.76× faster) | 132 ns (6.5× faster) | 485 ns (9.83× faster) | 1.18 µs (2.65× faster) | — | +| Doublets Split NonVolatile | 190 ns (29.8× faster) | 4.4 ns (28.8× faster) | 23.8 ns (76.9× faster) | 181 ns (10.1× faster) | 141 ns (13.1× faster) | 141 ns (13.1× faster) | 532 ns (34.5× faster) | 1.38 µs (7.43× faster) | 80.0 MiB | + +##### 10,000,000 links + +_1 repetition after a warm-up, median time per operation. SQLite 3.53.2, Ubuntu 24.04.5 LTS, INTEL(R) XEON(R) PLATINUM 8573C, 4 cores, 16 GiB, [GitHub Actions run](https://github.com/linksplatform/Comparisons.SQLiteVSDoublets/actions/runs/37226110636) on 2026-10-04._ + +| Storage | Create | Read all | Read by id | Search (from, to) | Read by from | Read by to | Update | Delete | File size | +| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | +| SQLite Memory | 3.34 µs | 105 ns | 1.62 µs | 1.62 µs | 1.57 µs | 1.5 µs | 9.58 µs | 6.3 µs | — | +| SQLite File | 7.41 µs | 106 ns | 1.81 µs | 2.05 µs | 1.98 µs | 1.97 µs | 18.3 µs | 10.9 µs | 501.0 MiB | +| Doublets United Volatile | 2.38 µs (1.41× faster) | 6.42 ns (16.4× faster) | 22.4 ns (72.3× faster) | 1.3 µs (1.25× faster) | 1.43 µs (1.1× faster) | 2.3 µs (1.53× slower) | 6.59 µs (1.45× faster) | 2.48 µs (2.54× faster) | — | +| Doublets United NonVolatile | 2.72 µs (2.72× faster) | 6.31 ns (16.9× faster) | 22.4 ns (81× faster) | 1.42 µs (1.44× faster) | 1.67 µs (1.19× faster) | 1.65 µs (1.19× faster) | 7.75 µs (2.36× faster) | 3.36 µs (3.25× faster) | 640.0 MiB | +| Doublets Split Volatile | 363 ns (9.19× faster) | 6.9 ns (15.3× faster) | 23.4 ns (69.3× faster) | 246 ns (6.59× faster) | 141 ns (11.1× faster) | 148 ns (10.1× faster) | 728 ns (13.2× faster) | 3.05 µs (2.07× faster) | — | +| Doublets Split NonVolatile | 644 ns (11.5× faster) | 7.01 ns (15.2× faster) | 26.2 ns (69.2× faster) | 240 ns (8.57× faster) | 143 ns (13.9× faster) | 145 ns (13.6× faster) | 709 ns (25.8× faster) | 3.62 µs (3.01× faster) | 800.0 MiB | -The comparison runs the same link workload against SQLite in-memory and file -databases and four Doublets layouts: united/split and volatile/non-volatile. -It measures create, update, delete, enumerate all, and queries by identity, -concrete `(source, target)`, outgoing source, and incoming target. The Rust -suite also measures creation, reading, and deletion of object-like blog posts -on every storage variant. +![Rust doublets vs SQLite, 64 bit, links](docs/benchmarks/links-rust-64.png) -Preparation and cleanup are outside the measured region. Pull requests run a -reduced-scale validation and preserve raw output, tables, and charts as -workflow artifacts. Full runs on `main` publish the tables and charts below -with a link to the producing workflow run. +### C# doublets vs SQLite -Run the checks locally: +#### 32 bit address/id space benchmarks -```bash -cd rust -cargo test --lib --tests -cargo check --benches -BENCHMARK_LINK_COUNT=10 BACKGROUND_LINK_COUNT=30 \ - BENCHMARK_OBJECT_COUNT=5 cargo bench --bench bench -- \ - --output-format bencher - -cd ../csharp -dotnet run -c Release -- --self-test -BENCHMARK_LINK_COUNT=10 BACKGROUND_LINK_COUNT=30 \ - dotnet run -c Release -- --filter '*LinksBenchmarks*' -``` +##### 100,000 links + +_10 repetitions after a warm-up, median time per operation. SQLite 3.53.3, Ubuntu 24.04.5 LTS, AMD EPYC 7763 64-Core Processor, 4 cores, 16 GiB, [GitHub Actions run](https://github.com/linksplatform/Comparisons.SQLiteVSDoublets/actions/runs/37226110636) on 2026-10-04._ + +| Storage | Create | Read all | Read by id | Search (from, to) | Read by from | Read by to | Update | Delete | File size | +| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | +| SQLite Memory | 6.27 µs | 380 ns | 1.2 µs | 1.46 µs | 1.51 µs | 1.5 µs | 4.27 µs | 2.84 µs | — | +| SQLite File | 5.75 µs | 385 ns | 1.21 µs | 1.48 µs | 1.52 µs | 1.52 µs | 12.2 µs | 6.22 µs | 4.4 MiB | +| Doublets United Volatile | 1.02 µs (6.12× faster) | 14.9 ns (25.5× faster) | 60.1 ns (20× faster) | 226 ns (6.45× faster) | 350 ns (4.32× faster) | 264 ns (5.67× faster) | 2.03 µs (2.1× faster) | 1.05 µs (2.7× faster) | — | +| Doublets United NonVolatile | 1.08 µs (5.32× faster) | 14.8 ns (26× faster) | 60.5 ns (20× faster) | 258 ns (5.73× faster) | 366 ns (4.17× faster) | 267 ns (5.68× faster) | 2.11 µs (5.76× faster) | 1.06 µs (5.89× faster) | 32.0 MiB | +| Doublets Split Volatile | 192 ns (32.7× faster) | 16 ns (23.7× faster) | 69.6 ns (17.2× faster) | 107 ns (13.7× faster) | 84.5 ns (17.9× faster) | 84.8 ns (17.7× faster) | 218 ns (19.6× faster) | 276 ns (10.3× faster) | — | +| Doublets Split NonVolatile | 241 ns (23.9× faster) | 16.5 ns (23.4× faster) | 69.1 ns (17.5× faster) | 110 ns (13.5× faster) | 86.7 ns (17.6× faster) | 86.4 ns (17.6× faster) | 225 ns (54.1× faster) | 263 ns (23.6× faster) | 40.0 MiB | + +##### 1,000,000 links + +_3 repetitions after a warm-up, median time per operation. SQLite 3.53.3, Ubuntu 24.04.5 LTS, Intel(R) Xeon(R) 6973P-C, 4 cores, 16 GiB, [GitHub Actions run](https://github.com/linksplatform/Comparisons.SQLiteVSDoublets/actions/runs/37226110636) on 2026-10-04._ -### C# link results +| Storage | Create | Read all | Read by id | Search (from, to) | Read by from | Read by to | Update | Delete | File size | +| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | +| SQLite Memory | 7.05 µs | 428 ns | 1.51 µs | 1.69 µs | 1.76 µs | 1.77 µs | 6.76 µs | 4.12 µs | — | +| SQLite File | 6.89 µs | 385 ns | 1.81 µs | 1.95 µs | 2.02 µs | 2.04 µs | 12.3 µs | 8.07 µs | 48.2 MiB | +| Doublets United Volatile | 1.7 µs (4.16× faster) | 16.4 ns (26.1× faster) | 117 ns (12.9× faster) | 574 ns (2.94× faster) | 691 ns (2.55× faster) | 824 ns (2.15× faster) | 4.3 µs (1.57× faster) | 1.85 µs (2.23× faster) | — | +| Doublets United NonVolatile | 1.85 µs (3.72× faster) | 16.5 ns (23.4× faster) | 123 ns (14.7× faster) | 688 ns (2.83× faster) | 861 ns (2.35× faster) | 982 ns (2.07× faster) | 4.93 µs (2.49× faster) | 2.25 µs (3.59× faster) | 32.0 MiB | +| Doublets Split Volatile | 400 ns (17.6× faster) | 16.8 ns (25.5× faster) | 231 ns (6.54× faster) | 396 ns (4.26× faster) | 324 ns (5.44× faster) | 338 ns (5.24× faster) | 594 ns (11.4× faster) | 674 ns (6.11× faster) | — | +| Doublets Split NonVolatile | 467 ns (14.8× faster) | 17.6 ns (21.9× faster) | 238 ns (7.58× faster) | 439 ns (4.43× faster) | 369 ns (5.49× faster) | 357 ns (5.7× faster) | 618 ns (19.9× faster) | 601 ns (13.4× faster) | 40.0 MiB | - -_Generated 2026-09-19 18:13 UTC for C# by [GitHub Actions run 35459522587](https://github.com/linksplatform/Comparisons.SQLiteVSDoublets/actions/runs/35459522587) — 1000 benchmarked links, 3000 background links._ +##### 10,000,000 links -| Operation | Doublets United Volatile | Doublets United NonVolatile | Doublets Split Volatile | Doublets Split NonVolatile | SQLite Memory | SQLite File | -|---------------|--------------------------|-----------------------------|-------------------------|----------------------------|---------------|-------------| -| Create | 2414327 (3.7x faster) | 2512520 (3.5x faster) | 867200 (10.2x faster) | 896999 (9.8x faster) | 8818051 | 2294476668 | -| Update | 4566053 (2.3x faster) | 4670899 (2.3x faster) | 1541871 (7.0x faster) | 1534119 (7.0x faster) | 10720700 | 2733072615 | -| Delete | 484008 (14.3x faster) | 465524 (14.8x faster) | 523910 (13.2x faster) | 521322 (13.3x faster) | 6907869 | 1682008343 | -| Each All | 437356 (3.7x faster) | 470885 (3.4x faster) | 420518 (3.8x faster) | 397666 (4.0x faster) | 2113072 | 1598861 | -| Each Identity | 430962 (11.6x faster) | 411173 (12.1x faster) | 403128 (12.4x faster) | 385531 (13.0x faster) | 4995738 | 12044311 | -| Each Concrete | 1363130 (5.2x faster) | 1408600 (5.1x faster) | 647769 (11.0x faster) | 917509 (7.8x faster) | 7134655 | 13622920 | -| Each Outgoing | 1160157 (5.3x faster) | 1180175 (5.3x faster) | 862790 (7.2x faster) | 830070 (7.5x faster) | 6199124 | 12923172 | -| Each Incoming | 1139559 (5.7x faster) | 1169325 (5.6x faster) | 796694 (8.2x faster) | 776158 (8.4x faster) | 6533836 | 13510164 | +_1 repetition after a warm-up, median time per operation. SQLite 3.53.3, Ubuntu 24.04.5 LTS, AMD EPYC 9V74 80-Core Processor, 4 cores, 16 GiB, [GitHub Actions run](https://github.com/linksplatform/Comparisons.SQLiteVSDoublets/actions/runs/37226110636) on 2026-10-04._ -![C# benchmark comparison](docs/benchmarks/bench_csharp.png) +| Storage | Create | Read all | Read by id | Search (from, to) | Read by from | Read by to | Update | Delete | File size | +| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | +| SQLite Memory | 7.41 µs | 316 ns | 1.86 µs | 2.01 µs | 2.06 µs | 2.06 µs | 9.49 µs | 6.49 µs | — | +| SQLite File | 18.2 µs | 318 ns | 2.72 µs | 3.16 µs | 3.11 µs | 3.1 µs | 24.6 µs | 14.8 µs | 501.0 MiB | +| Doublets United Volatile | 3.01 µs (2.46× faster) | 10 ns (31.6× faster) | 181 ns (10.3× faster) | 1.45 µs (1.39× faster) | 1.67 µs (1.23× faster) | 1.88 µs (1.1× faster) | 8.16 µs (1.16× faster) | 3.52 µs (1.85× faster) | — | +| Doublets United NonVolatile | 3.95 µs (4.61× faster) | 14.1 ns (22.6× faster) | 187 ns (14.5× faster) | 1.44 µs (2.19× faster) | 1.64 µs (1.9× faster) | 1.93 µs (1.61× faster) | 8.99 µs (2.74× faster) | 4.35 µs (3.39× faster) | 320.0 MiB | +| Doublets Split Volatile | 431 ns (17.2× faster) | 10.3 ns (30.8× faster) | 199 ns (9.3× faster) | 475 ns (4.23× faster) | 323 ns (6.38× faster) | 324 ns (6.37× faster) | 795 ns (11.9× faster) | 574 ns (11.3× faster) | — | +| Doublets Split NonVolatile | 1.16 µs (15.7× faster) | 9.1 ns (35× faster) | 211 ns (12.9× faster) | 508 ns (6.21× faster) | 354 ns (8.79× faster) | 357 ns (8.69× faster) | 1.01 µs (24.5× faster) | 780 ns (18.9× faster) | 400.0 MiB | -![C# benchmark comparison, logarithmic scale](docs/benchmarks/bench_csharp_log_scale.png) - +![C# doublets vs SQLite, 32 bit, links](docs/benchmarks/links-csharp-32.png) -### Rust link and object results +#### 64 bit address/id space benchmarks + +##### 100,000 links - -_Generated 2026-09-19 18:46 UTC for Rust by [GitHub Actions run 35459522519](https://github.com/linksplatform/Comparisons.SQLiteVSDoublets/actions/runs/35459522519) — 1000 benchmarked links, 3000 background links, 1000 objects._ +_10 repetitions after a warm-up, median time per operation. SQLite 3.53.3, Ubuntu 24.04.5 LTS, Intel(R) Xeon(R) 6973P-C, 4 cores, 16 GiB, [GitHub Actions run](https://github.com/linksplatform/Comparisons.SQLiteVSDoublets/actions/runs/37226110636) on 2026-10-04._ -| Operation | Doublets United Volatile | Doublets United NonVolatile | Doublets Split Volatile | Doublets Split NonVolatile | SQLite Memory | SQLite File | -|---------------------|--------------------------|-----------------------------|-------------------------|----------------------------|---------------|-------------| -| Create | 141747 (40.7x faster) | 143659 (40.2x faster) | 69524 (83.0x faster) | 69702 (82.8x faster) | 5772002 | 993908545 | -| Update | 283809 (29.6x faster) | 277912 (30.2x faster) | 63882 (131.3x faster) | 72899 (115.1x faster) | 8390541 | 1067410860 | -| Delete | 168149 (26.8x faster) | 166496 (27.1x faster) | 77711 (58.0x faster) | 78217 (57.6x faster) | 4509130 | 977528491 | -| Each All | 31671 (15.9x faster) | 31638 (15.9x faster) | 34426 (14.6x faster) | 34487 (14.6x faster) | 504031 | 595624 | -| Each Identity | 2132 (1269.3x faster) | 2136 (1266.9x faster) | 2130 (1270.5x faster) | 2127 (1272.3x faster) | 2706180 | 10678850 | -| Each Concrete | 124784 (34.7x faster) | 120544 (36.0x faster) | 45647 (95.0x faster) | 43793 (99.0x faster) | 4335748 | 12668554 | -| Each Outgoing | 210057 (18.1x faster) | 208064 (18.2x faster) | 44112 (86.0x faster) | 45302 (83.7x faster) | 3792959 | 12033519 | -| Each Incoming | 212266 (19.3x faster) | 210393 (19.4x faster) | 50873 (80.4x faster) | 46396 (88.1x faster) | 4089215 | 12477405 | -| Objects Create List | 8759735 (10.5x slower) | 8759735 (10.5x slower) | 5388349 (6.5x slower) | 5436404 (6.5x slower) | 832974 | 2098461 | -| Objects Read List | 5889568 (18.5x slower) | 5585959 (17.6x slower) | 5393697 (17.0x slower) | 4889083 (15.4x slower) | 317868 | 343282 | -| Objects Delete List | 2709133 (75.1x slower) | 2667861 (74.0x slower) | 1083231 (30.0x slower) | 1106546 (30.7x slower) | 36069 | 992614 | +| Storage | Create | Read all | Read by id | Search (from, to) | Read by from | Read by to | Update | Delete | File size | +| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | +| SQLite Memory | 4.63 µs | 355 ns | 882 ns | 1.07 µs | 1.12 µs | 1.13 µs | 3.45 µs | 2.3 µs | — | +| SQLite File | 3.83 µs | 363 ns | 844 ns | 1.03 µs | 1.07 µs | 1.08 µs | 7.17 µs | 4.04 µs | 4.4 MiB | +| Doublets United Volatile | 1.12 µs (4.12× faster) | 19.6 ns (18.1× faster) | 65 ns (13.6× faster) | 223 ns (4.81× faster) | 265 ns (4.23× faster) | 371 ns (3.04× faster) | 2.49 µs (1.39× faster) | 1.24 µs (1.86× faster) | — | +| Doublets United NonVolatile | 1.2 µs (3.19× faster) | 20.8 ns (17.4× faster) | 73 ns (11.6× faster) | 279 ns (3.68× faster) | 360 ns (2.96× faster) | 299 ns (3.61× faster) | 2.57 µs (2.8× faster) | 1.24 µs (3.26× faster) | 64.0 MiB | +| Doublets Split Volatile | 247 ns (18.8× faster) | 15.4 ns (23.1× faster) | 91.2 ns (9.67× faster) | 130 ns (8.23× faster) | 118 ns (9.54× faster) | 113 ns (9.98× faster) | 234 ns (14.8× faster) | 296 ns (7.76× faster) | — | +| Doublets Split NonVolatile | 278 ns (13.8× faster) | 14 ns (26× faster) | 86.6 ns (9.74× faster) | 128 ns (8.03× faster) | 118 ns (9.03× faster) | 116 ns (9.32× faster) | 236 ns (30.4× faster) | 266 ns (15.2× faster) | 80.0 MiB | + +##### 1,000,000 links + +_3 repetitions after a warm-up, median time per operation. SQLite 3.53.3, Ubuntu 24.04.5 LTS, AMD EPYC 7763 64-Core Processor, 4 cores, 16 GiB, [GitHub Actions run](https://github.com/linksplatform/Comparisons.SQLiteVSDoublets/actions/runs/37226110636) on 2026-10-04._ + +| Storage | Create | Read all | Read by id | Search (from, to) | Read by from | Read by to | Update | Delete | File size | +| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | +| SQLite Memory | 7.39 µs | 386 ns | 1.49 µs | 1.8 µs | 1.83 µs | 1.83 µs | 7.12 µs | 4.86 µs | — | +| SQLite File | 9.97 µs | 386 ns | 2.64 µs | 2.76 µs | 2.84 µs | 2.86 µs | 21 µs | 11.7 µs | 48.2 MiB | +| Doublets United Volatile | 2.15 µs (3.44× faster) | 22.3 ns (17.3× faster) | 162 ns (9.17× faster) | 576 ns (3.12× faster) | 687 ns (2.66× faster) | 918 ns (2× faster) | 5.14 µs (1.39× faster) | 2.32 µs (2.1× faster) | — | +| Doublets United NonVolatile | 2.2 µs (4.54× faster) | 21.6 ns (17.8× faster) | 169 ns (15.6× faster) | 710 ns (3.89× faster) | 826 ns (3.44× faster) | 1.01 µs (2.83× faster) | 5.66 µs (3.7× faster) | 2.58 µs (4.55× faster) | 64.0 MiB | +| Doublets Split Volatile | 366 ns (20.2× faster) | 20.8 ns (18.6× faster) | 220 ns (6.75× faster) | 328 ns (5.49× faster) | 280 ns (6.51× faster) | 282 ns (6.51× faster) | 658 ns (10.8× faster) | 567 ns (8.57× faster) | — | +| Doublets Split NonVolatile | 458 ns (21.8× faster) | 21.2 ns (18.2× faster) | 245 ns (10.8× faster) | 369 ns (7.48× faster) | 317 ns (8.97× faster) | 323 ns (8.84× faster) | 716 ns (29.3× faster) | 580 ns (20.2× faster) | 80.0 MiB | -![Rust benchmark comparison](docs/benchmarks/bench_rust.png) +##### 10,000,000 links -![Rust benchmark comparison, logarithmic scale](docs/benchmarks/bench_rust_log_scale.png) - +_1 repetition after a warm-up, median time per operation. SQLite 3.53.3, Ubuntu 24.04.5 LTS, AMD EPYC 7763 64-Core Processor, 4 cores, 16 GiB, [GitHub Actions run](https://github.com/linksplatform/Comparisons.SQLiteVSDoublets/actions/runs/37226110636) on 2026-10-04._ -The original C# object comparison and its historical results remain below. +| Storage | Create | Read all | Read by id | Search (from, to) | Read by from | Read by to | Update | Delete | File size | +| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | +| SQLite Memory | 8.4 µs | 384 ns | 1.89 µs | 2.21 µs | 2.18 µs | 2.16 µs | 9.45 µs | 6.63 µs | — | +| SQLite File | 20 µs | 383 ns | 2.88 µs | 3.38 µs | 3.32 µs | 3.32 µs | 25.4 µs | 15.3 µs | 501.0 MiB | +| Doublets United Volatile | 3.04 µs (2.76× faster) | 13.9 ns (27.7× faster) | 180 ns (10.5× faster) | 1.31 µs (1.69× faster) | 1.5 µs (1.45× faster) | 1.75 µs (1.24× faster) | 8.6 µs (1.1× faster) | 3.84 µs (1.72× faster) | — | +| Doublets United NonVolatile | 4.43 µs (4.52× faster) | 21.6 ns (17.7× faster) | 172 ns (16.8× faster) | 1.36 µs (2.49× faster) | 1.55 µs (2.14× faster) | 1.65 µs (2.01× faster) | 9.64 µs (2.63× faster) | 4.78 µs (3.19× faster) | 640.0 MiB | +| Doublets Split Volatile | 458 ns (18.4× faster) | 13 ns (29.5× faster) | 308 ns (6.12× faster) | 445 ns (4.97× faster) | 329 ns (6.64× faster) | 350 ns (6.17× faster) | 908 ns (10.4× faster) | 725 ns (9.14× faster) | — | +| Doublets Split NonVolatile | 1.26 µs (15.8× faster) | 11.9 ns (32.3× faster) | 326 ns (8.84× faster) | 467 ns (7.24× faster) | 332 ns (10× faster) | 345 ns (9.64× faster) | 1.05 µs (24.2× faster) | 875 ns (17.4× faster) | 800.0 MiB | + +![C# doublets vs SQLite, 64 bit, links](docs/benchmarks/links-csharp-64.png) + +## Doublets vs SQLite as storage for objects + +### Rust doublets vs SQLite + +#### 32 bit address/id space benchmarks + +##### 100,000 blog posts + +_5 repetitions after a warm-up, median time per operation. SQLite 3.53.2, Ubuntu 24.04.5 LTS, Intel(R) Xeon(R) 6973P-C, 4 cores, 16 GiB, [GitHub Actions run](https://github.com/linksplatform/Comparisons.SQLiteVSDoublets/actions/runs/37226110636) on 2026-10-04._ + +| Storage | Create | Read all | Read by id | Delete | File size | +| --- | ---: | ---: | ---: | ---: | ---: | +| SQLite Memory | 838 ns | 485 ns | 1.37 µs | 1.49 µs | — | +| SQLite File | 4.96 µs | 476 ns | 1.63 µs | 8.03 µs | 78.3 MiB | +| Doublets United Volatile Cached | 4.58 µs (5.46× slower) | 1.35 µs (2.79× slower) | 2 µs (1.46× slower) | 4.51 µs (3.03× slower) | — | +| Doublets United Volatile Uncached | 95.5 µs (114× slower) | 7.59 µs (15.7× slower) | 8.21 µs (5.99× slower) | 4.52 µs (3.04× slower) | — | +| Doublets United NonVolatile Cached | 4.87 µs (≈ same) | 1.41 µs (2.96× slower) | 2.25 µs (1.38× slower) | 4.82 µs (1.66× faster) | 32.0 MiB | +| Doublets United NonVolatile Uncached | 92.7 µs (18.7× slower) | 7.8 µs (16.4× slower) | 8.54 µs (5.24× slower) | 4.86 µs (1.65× faster) | 32.0 MiB | +| Doublets Split Volatile Cached | 2.89 µs (3.45× slower) | 698 ns (1.44× slower) | 601 ns (2.28× faster) | 2.18 µs (1.47× slower) | — | +| Doublets Split Volatile Uncached | 36.3 µs (43.3× slower) | 9.78 µs (20.2× slower) | 10.1 µs (7.36× slower) | 2.11 µs (1.42× slower) | — | +| Doublets Split NonVolatile Cached | 2.84 µs (1.75× faster) | 641 ns (1.35× slower) | 588 ns (2.77× faster) | 2.33 µs (3.45× faster) | 40.0 MiB | +| Doublets Split NonVolatile Uncached | 35.9 µs (7.23× slower) | 9.33 µs (19.6× slower) | 9.69 µs (5.94× slower) | 2.37 µs (3.39× faster) | 40.0 MiB | + +##### 1,000,000 blog posts + +_1 repetition after a warm-up, median time per operation. SQLite 3.53.2, Ubuntu 24.04.5 LTS, INTEL(R) XEON(R) PLATINUM 8573C, 4 cores, 16 GiB, [GitHub Actions run](https://github.com/linksplatform/Comparisons.SQLiteVSDoublets/actions/runs/37226110636) on 2026-10-04._ + +| Storage | Create | Read all | Read by id | Delete | File size | +| --- | ---: | ---: | ---: | ---: | ---: | +| SQLite Memory | 965 ns | 478 ns | 2.06 µs | 2.7 µs | — | +| SQLite File | 5.93 µs | 470 ns | 2.71 µs | 11.1 µs | 783.2 MiB | +| Doublets United Volatile Cached | 6.88 µs (7.13× slower) | 1.64 µs (3.42× slower) | 3.23 µs (1.56× slower) | 10.7 µs (3.97× slower) | — | +| Doublets United Volatile Uncached | 113 µs (117× slower) | 8.34 µs (17.4× slower) | 10.3 µs (5× slower) | 9.72 µs (3.6× slower) | — | +| Doublets United NonVolatile Cached | 7.85 µs (1.32× slower) | 1.61 µs (3.43× slower) | 3.15 µs (1.16× slower) | 10.4 µs (1.06× faster) | 320.0 MiB | +| Doublets United NonVolatile Uncached | 121 µs (20.3× slower) | 8.46 µs (18× slower) | 10.4 µs (3.83× slower) | 10.4 µs (1.06× faster) | 320.0 MiB | +| Doublets Split Volatile Cached | 4.16 µs (4.32× slower) | 914 ns (1.91× slower) | 1.06 µs (1.95× faster) | 4.99 µs (1.85× slower) | — | +| Doublets Split Volatile Uncached | 39.3 µs (40.7× slower) | 11.2 µs (23.4× slower) | 11.7 µs (5.69× slower) | 4.8 µs (1.78× slower) | — | +| Doublets Split NonVolatile Cached | 5.52 µs (1.08× faster) | 913 ns (1.94× slower) | 1.12 µs (2.42× faster) | 5.7 µs (1.94× faster) | 400.0 MiB | +| Doublets Split NonVolatile Uncached | 41.6 µs (7.02× slower) | 11.1 µs (23.7× slower) | 11.7 µs (4.3× slower) | 5.88 µs (1.88× faster) | 400.0 MiB | + +![Rust doublets vs SQLite, 32 bit, objects](docs/benchmarks/objects-rust-32.png) + +#### 64 bit address/id space benchmarks + +##### 100,000 blog posts + +_5 repetitions after a warm-up, median time per operation. SQLite 3.53.2, Ubuntu 24.04.5 LTS, AMD EPYC 7763 64-Core Processor, 4 cores, 16 GiB, [GitHub Actions run](https://github.com/linksplatform/Comparisons.SQLiteVSDoublets/actions/runs/37226110636) on 2026-10-04._ + +| Storage | Create | Read all | Read by id | Delete | File size | +| --- | ---: | ---: | ---: | ---: | ---: | +| SQLite Memory | 1.22 µs | 548 ns | 1.44 µs | 1.7 µs | — | +| SQLite File | 2.01 µs | 625 ns | 2.6 µs | 7.27 µs | 78.3 MiB | +| Doublets United Volatile Cached | 5.17 µs (4.22× slower) | 1.46 µs (2.67× slower) | 2.25 µs (1.57× slower) | 6.12 µs (3.6× slower) | — | +| Doublets United Volatile Uncached | 95.7 µs (78.2× slower) | 9.79 µs (17.9× slower) | 10.7 µs (7.47× slower) | 5.33 µs (3.13× slower) | — | +| Doublets United NonVolatile Cached | 5.58 µs (2.78× slower) | 1.45 µs (2.32× slower) | 2.13 µs (1.22× faster) | 5.65 µs (1.29× faster) | 64.0 MiB | +| Doublets United NonVolatile Uncached | 99.7 µs (49.7× slower) | 9.73 µs (15.6× slower) | 11 µs (4.22× slower) | 5.68 µs (1.28× faster) | 64.0 MiB | +| Doublets Split Volatile Cached | 3.39 µs (2.77× slower) | 771 ns (1.41× slower) | 608 ns (2.37× faster) | 2.94 µs (1.73× slower) | — | +| Doublets Split Volatile Uncached | 48.1 µs (39.3× slower) | 14.1 µs (25.7× slower) | 14.5 µs (10.1× slower) | 2.97 µs (1.75× slower) | — | +| Doublets Split NonVolatile Cached | 3.75 µs (1.87× slower) | 801 ns (1.28× slower) | 634 ns (4.09× faster) | 3.68 µs (1.98× faster) | 80.0 MiB | +| Doublets Split NonVolatile Uncached | 47.4 µs (23.6× slower) | 14.1 µs (22.5× slower) | 14.6 µs (5.6× slower) | 3.23 µs (2.25× faster) | 80.0 MiB | + +##### 1,000,000 blog posts + +_1 repetition after a warm-up, median time per operation. SQLite 3.53.2, Ubuntu 24.04.5 LTS, INTEL(R) XEON(R) PLATINUM 8573C, 4 cores, 16 GiB, [GitHub Actions run](https://github.com/linksplatform/Comparisons.SQLiteVSDoublets/actions/runs/37226110636) on 2026-10-04._ + +| Storage | Create | Read all | Read by id | Delete | File size | +| --- | ---: | ---: | ---: | ---: | ---: | +| SQLite Memory | 971 ns | 481 ns | 2.07 µs | 2.66 µs | — | +| SQLite File | 5.28 µs | 466 ns | 2.69 µs | 10.8 µs | 783.2 MiB | +| Doublets United Volatile Cached | 7.42 µs (7.64× slower) | 1.77 µs (3.69× slower) | 3.27 µs (1.58× slower) | 11 µs (4.13× slower) | — | +| Doublets United Volatile Uncached | 114 µs (117× slower) | 8.55 µs (17.8× slower) | 10.8 µs (5.19× slower) | 11.8 µs (4.44× slower) | — | +| Doublets United NonVolatile Cached | 9.17 µs (1.74× slower) | 1.72 µs (3.69× slower) | 3.45 µs (1.28× slower) | 11.8 µs (1.1× slower) | 640.0 MiB | +| Doublets United NonVolatile Uncached | 124 µs (23.4× slower) | 9.19 µs (19.7× slower) | 11.2 µs (4.17× slower) | 12.3 µs (1.14× slower) | 640.0 MiB | +| Doublets Split Volatile Cached | 4.83 µs (4.98× slower) | 919 ns (1.91× slower) | 1.19 µs (1.74× faster) | 6.82 µs (2.56× slower) | — | +| Doublets Split Volatile Uncached | 43.2 µs (44.5× slower) | 11 µs (22.9× slower) | 11.7 µs (5.65× slower) | 5.93 µs (2.23× slower) | — | +| Doublets Split NonVolatile Cached | 6.98 µs (1.32× slower) | 896 ns (1.92× slower) | 1.23 µs (2.18× faster) | 7.21 µs (1.49× faster) | 800.0 MiB | +| Doublets Split NonVolatile Uncached | 46.4 µs (8.78× slower) | 11.3 µs (24.3× slower) | 12 µs (4.45× slower) | 7.58 µs (1.42× faster) | 800.0 MiB | + +![Rust doublets vs SQLite, 64 bit, objects](docs/benchmarks/objects-rust-64.png) + +### C# doublets vs SQLite + +#### 32 bit address/id space benchmarks + +##### 100,000 blog posts + +_5 repetitions after a warm-up, median time per operation. SQLite 3.53.3, Ubuntu 24.04.5 LTS, AMD EPYC 7763 64-Core Processor, 4 cores, 16 GiB, [GitHub Actions run](https://github.com/linksplatform/Comparisons.SQLiteVSDoublets/actions/runs/37226110636) on 2026-10-04._ + +| Storage | Create | Read all | Read by id | Delete | File size | +| --- | ---: | ---: | ---: | ---: | ---: | +| SQLite Memory | 5.16 µs | 1.51 µs | 2.86 µs | 2.09 µs | — | +| SQLite File | 6.4 µs | 1.6 µs | 4.03 µs | 8.14 µs | 78.3 MiB | +| Doublets United Volatile Cached | 16.7 µs (3.23× slower) | 8.6 µs (5.68× slower) | 2.77 µs (≈ same) | 11.7 µs (5.59× slower) | — | +| Doublets United Volatile Uncached | 178 µs (34.5× slower) | 322 µs (213× slower) | 324 µs (113× slower) | 10.7 µs (5.13× slower) | — | +| Doublets United NonVolatile Cached | 17 µs (2.65× slower) | 8.53 µs (5.31× slower) | 2.88 µs (1.4× faster) | 11.6 µs (1.43× slower) | 32.0 MiB | +| Doublets United NonVolatile Uncached | 178 µs (27.8× slower) | 330 µs (205× slower) | 331 µs (82.2× slower) | 12.4 µs (1.52× slower) | 32.0 MiB | +| Doublets Split Volatile Cached | 8.93 µs (1.73× slower) | 9.8 µs (6.48× slower) | 4.17 µs (1.46× slower) | 7.87 µs (3.77× slower) | — | +| Doublets Split Volatile Uncached | 113 µs (21.8× slower) | 389 µs (257× slower) | 392 µs (137× slower) | 7.86 µs (3.76× slower) | — | +| Doublets Split NonVolatile Cached | 9.64 µs (1.51× slower) | 9.74 µs (6.07× slower) | 4.26 µs (1.06× slower) | 8.29 µs (≈ same) | 40.0 MiB | +| Doublets Split NonVolatile Uncached | 110 µs (17.2× slower) | 379 µs (236× slower) | 381 µs (94.6× slower) | 8.03 µs (≈ same) | 40.0 MiB | + +##### 1,000,000 blog posts + +_1 repetition after a warm-up, median time per operation. SQLite 3.53.3, Ubuntu 24.04.5 LTS, AMD EPYC 9V74 80-Core Processor, 4 cores, 16 GiB, [GitHub Actions run](https://github.com/linksplatform/Comparisons.SQLiteVSDoublets/actions/runs/37226110636) on 2026-10-04._ + +| Storage | Create | Read all | Read by id | Delete | File size | +| --- | ---: | ---: | ---: | ---: | ---: | +| SQLite Memory | 4.17 µs | 1.24 µs | 3.03 µs | 2.93 µs | — | +| SQLite File | 8.39 µs | 1.27 µs | 4.81 µs | 16 µs | 783.2 MiB | +| Doublets United Volatile Cached | 16.5 µs (3.96× slower) | 7.17 µs (5.77× slower) | 3.77 µs (1.25× slower) | 14 µs (4.77× slower) | — | +| Doublets United Volatile Uncached | 192 µs (46× slower) | 248 µs (200× slower) | 251 µs (82.9× slower) | 14.1 µs (4.8× slower) | — | +| Doublets United NonVolatile Cached | 23.3 µs (2.77× slower) | 7.07 µs (5.59× slower) | 4.05 µs (1.19× faster) | 14.3 µs (1.12× faster) | 320.0 MiB | +| Doublets United NonVolatile Uncached | 196 µs (23.3× slower) | 250 µs (197× slower) | 251 µs (52.2× slower) | 14.7 µs (1.09× faster) | 320.0 MiB | +| Doublets Split Volatile Cached | 7.97 µs (1.91× slower) | 8.1 µs (6.52× slower) | 3.82 µs (1.26× slower) | 9.2 µs (3.14× slower) | — | +| Doublets Split Volatile Uncached | 87.2 µs (20.9× slower) | 335 µs (270× slower) | 327 µs (108× slower) | 9.22 µs (3.15× slower) | — | +| Doublets Split NonVolatile Cached | 15.6 µs (1.86× slower) | 8.73 µs (6.9× slower) | 3.9 µs (1.23× faster) | 12.9 µs (1.25× faster) | 400.0 MiB | +| Doublets Split NonVolatile Uncached | 98.9 µs (11.8× slower) | 320 µs (253× slower) | 323 µs (67.2× slower) | 10.2 µs (1.57× faster) | 400.0 MiB | + +![C# doublets vs SQLite, 32 bit, objects](docs/benchmarks/objects-csharp-32.png) + +#### 64 bit address/id space benchmarks + +##### 100,000 blog posts + +_5 repetitions after a warm-up, median time per operation. SQLite 3.53.3, Ubuntu 24.04.5 LTS, AMD EPYC 7763 64-Core Processor, 4 cores, 16 GiB, [GitHub Actions run](https://github.com/linksplatform/Comparisons.SQLiteVSDoublets/actions/runs/37226110636) on 2026-10-04._ + +| Storage | Create | Read all | Read by id | Delete | File size | +| --- | ---: | ---: | ---: | ---: | ---: | +| SQLite Memory | 5.06 µs | 1.43 µs | 2.73 µs | 1.9 µs | — | +| SQLite File | 6.09 µs | 1.56 µs | 3.81 µs | 7.65 µs | 78.3 MiB | +| Doublets United Volatile Cached | 17.8 µs (3.52× slower) | 9.46 µs (6.62× slower) | 2.83 µs (≈ same) | 12.7 µs (6.7× slower) | — | +| Doublets United Volatile Uncached | 187 µs (36.9× slower) | 346 µs (243× slower) | 362 µs (133× slower) | 12.6 µs (6.65× slower) | — | +| Doublets United NonVolatile Cached | 18.5 µs (3.04× slower) | 9.61 µs (6.16× slower) | 3.09 µs (1.24× faster) | 12.9 µs (1.69× slower) | 64.0 MiB | +| Doublets United NonVolatile Uncached | 183 µs (30.1× slower) | 351 µs (225× slower) | 356 µs (93.3× slower) | 13.2 µs (1.72× slower) | 64.0 MiB | +| Doublets Split Volatile Cached | 8.24 µs (1.63× slower) | 9.57 µs (6.71× slower) | 4.19 µs (1.53× slower) | 7.52 µs (3.96× slower) | — | +| Doublets Split Volatile Uncached | 109 µs (21.5× slower) | 383 µs (268× slower) | 389 µs (142× slower) | 7.54 µs (3.97× slower) | — | +| Doublets Split NonVolatile Cached | 9.47 µs (1.56× slower) | 9.56 µs (6.13× slower) | 4.11 µs (1.08× slower) | 7.7 µs (≈ same) | 80.0 MiB | +| Doublets Split NonVolatile Uncached | 108 µs (17.8× slower) | 377 µs (242× slower) | 385 µs (101× slower) | 8.35 µs (1.09× slower) | 80.0 MiB | + +##### 1,000,000 blog posts + +_1 repetition after a warm-up, median time per operation. SQLite 3.53.3, Ubuntu 24.04.5 LTS, AMD EPYC 7763 64-Core Processor, 4 cores, 16 GiB, [GitHub Actions run](https://github.com/linksplatform/Comparisons.SQLiteVSDoublets/actions/runs/37226110636) on 2026-10-04._ + +| Storage | Create | Read all | Read by id | Delete | File size | +| --- | ---: | ---: | ---: | ---: | ---: | +| SQLite Memory | 5.71 µs | 1.59 µs | 3.41 µs | 3.18 µs | — | +| SQLite File | 7.73 µs | 1.66 µs | 5.17 µs | 11.5 µs | 783.2 MiB | +| Doublets United Volatile Cached | 22.3 µs (3.9× slower) | 9.91 µs (6.22× slower) | 4.25 µs (1.25× slower) | 20.3 µs (6.38× slower) | — | +| Doublets United Volatile Uncached | 232 µs (40.7× slower) | 318 µs (200× slower) | 320 µs (93.9× slower) | 18.2 µs (5.73× slower) | — | +| Doublets United NonVolatile Cached | 29.2 µs (3.78× slower) | 9.25 µs (5.58× slower) | 4.04 µs (1.28× faster) | 18.3 µs (1.59× slower) | 640.0 MiB | +| Doublets United NonVolatile Uncached | 236 µs (30.5× slower) | 315 µs (190× slower) | 319 µs (61.8× slower) | 20.7 µs (1.8× slower) | 640.0 MiB | +| Doublets Split Volatile Cached | 10.6 µs (1.85× slower) | 10.3 µs (6.46× slower) | 4.65 µs (1.36× slower) | 12 µs (3.76× slower) | — | +| Doublets Split Volatile Uncached | 111 µs (19.4× slower) | 387 µs (243× slower) | 405 µs (119× slower) | 13 µs (4.09× slower) | — | +| Doublets Split NonVolatile Cached | 20.4 µs (2.64× slower) | 11 µs (6.61× slower) | 5.27 µs (≈ same) | 15 µs (1.3× slower) | 800.0 MiB | +| Doublets Split NonVolatile Uncached | 132 µs (17.1× slower) | 402 µs (242× slower) | 407 µs (78.7× slower) | 16.6 µs (1.45× slower) | 800.0 MiB | + +![C# doublets vs SQLite, 64 bit, objects](docs/benchmarks/objects-csharp-64.png) + + + +## Original comparison + +The original C# object comparison and its historical results. + + + +### SQLite -## SQLite ```C# using System.Linq; using Comparisons.SQLiteVSDoublets.Model; @@ -129,7 +500,8 @@ namespace Comparisons.SQLiteVSDoublets.SQLite } ``` -## Doublets +### Doublets + ``` C# using System.IO; using Platform.IO; @@ -187,18 +559,22 @@ namespace Comparisons.SQLiteVSDoublets.Doublets } ``` -## [Result](https://www.icloud.com/keynote/0cYVNWkWD5RLU0k-XIBs3qWkA#Sqlite_vs_Doublets) +### [Result](https://www.icloud.com/keynote/0cYVNWkWD5RLU0k-XIBs3qWkA#Sqlite_vs_Doublets) + +#### Performance -### Performance ![Image with result of performance comparison between SQLite and Doublets.](https://raw.githubusercontent.com/linksplatform/Documentation/master/doc/Examples/sqlite_vs_doublets_performance.png "Result of performance comparison between SQLite and Doublets") -### Disk usage +#### Disk usage + ![Image with result of disk usage comparison between SQLite and Doublets.](https://raw.githubusercontent.com/linksplatform/Documentation/master/doc/Examples/sqlite_vs_doublets_disk_usage.png "Result of disk usage comparison between SQLite and Doublets") -### RAM usage +#### RAM usage + ![Image with result of RAM usage comparison between SQLite and Doublets.](https://raw.githubusercontent.com/linksplatform/Documentation/master/doc/Examples/sqlite_vs_doublets_ram_usage.png "Result of RAM usage comparison between SQLite and Doublets") -### Source data +#### Source data + ``` ini BenchmarkDotNet=v0.12.0, OS=Windows 10.0.18362 @@ -211,6 +587,7 @@ InvocationCount=1 IterationCount=1 UnrollFactor=1 WarmupCount=2 ``` + | Method | N | Mean | Error | Gen 0 | Gen 1 | Gen 2 | Allocated | SizeAfterCreation | |--------- |------- |------------:|------:|-------------:|------------:|------:|------------:|------------------:| | **SQLite** | **1000** | **719.1 ms** | **NA** | **5000.0000** | **-** | **-** | **30.67 MB** | **925696** | @@ -220,6 +597,9 @@ WarmupCount=2 | **SQLite** | **100000** | **35,853.8 ms** | **NA** | **680000.0000** | **151000.0000** | **-** | **3234.09 MB** | **90890240** | | Doublets | 100000 | 13,083.4 ms | NA | 3088000.0000 | 328000.0000 | - | 12356.33 MB | 64192256 | -## Conclusion + + +### Conclusion -In this particular comparison, Doublets are faster and use less memory on disk, but this comes with the cost of additional use of RAM (Sqlite uses it less). +In this particular comparison, Doublets are faster and use less memory on disk, +but this comes with the cost of additional use of RAM (Sqlite uses it less). diff --git a/README.ru.md b/README.ru.md index 312a64b..761f35d 100644 --- a/README.ru.md +++ b/README.ru.md @@ -1,91 +1,465 @@ -[![Состояние сборки](https://github.com/linksplatform/Comparisons.SQLiteVSDoublets/workflows/CI/badge.svg)](https://github.com/linksplatform/Comparisons.SQLiteVSDoublets/actions?workflow=CI) - # Comparisons.SQLiteVSDoublets ([english version](README.md)) +[![Rust](https://github.com/linksplatform/Comparisons.SQLiteVSDoublets/actions/workflows/rust.yml/badge.svg)](https://github.com/linksplatform/Comparisons.SQLiteVSDoublets/actions/workflows/rust.yml) +[![C#](https://github.com/linksplatform/Comparisons.SQLiteVSDoublets/actions/workflows/csharp.yml/badge.svg)](https://github.com/linksplatform/Comparisons.SQLiteVSDoublets/actions/workflows/csharp.yml) +[![Benchmarks](https://github.com/linksplatform/Comparisons.SQLiteVSDoublets/actions/workflows/benchmarks.yml/badge.svg)](https://github.com/linksplatform/Comparisons.SQLiteVSDoublets/actions/workflows/benchmarks.yml) + ![Сравнение моделей данных](https://github.com/LinksPlatform/Documentation/raw/master/doc/ModelsComparison/relational_model_vs_associative_model_vs_links_ru.png) -Сравнение SQLite и Дуплетов ПлатформыСвязей на базовых операциях встроенных баз данных со связями и объектоподобными структурами. +Сравнение SQLite и Дуплетов ПлатформыСвязей на базовых операциях встроенных +баз данных со связями и объектоподобными структурами. + +Основано на примерах из и + + +## Тесты производительности + +Rust ([doublets](https://crates.io/crates/doublets) и +[rusqlite](https://crates.io/crates/rusqlite) со встроенной SQLite) и C# +([Platform.Data.Doublets](https://www.nuget.org/packages/Platform.Data.Doublets), +[Platform.Data.Doublets.Sequences](https://www.nuget.org/packages/Platform.Data.Doublets.Sequences) +и [Microsoft.Data.Sqlite](https://www.nuget.org/packages/Microsoft.Data.Sqlite)) +выполняют одинаковую нагрузку на одинаковых детерминированных данных +с 32-битными (`u32`/`uint`) и 64-битными (`u64`/`ulong`) идентификаторами: + +- **Связи**: SQLite хранит таблицу + `links(id INTEGER PRIMARY KEY, "from", "to")` с индексами `("from", "to")` + и `("to", "from")`; Дуплеты хранят сами связи. Операции: создание, чтение + всех, чтение по id, поиск по `(from, to)`, чтение по `from`, чтение по `to`, + обновление (обмен `from` и `to`) и удаление. +- **Объекты**: записи блога с заголовком, содержимым и датой публикации. + SQLite хранит таблицу `blog_posts(id, title, content, publication_date)`; + Дуплеты хранят каждую запись в виде связей, а строки — в виде + последовательностей символов Юникода, как это делает + [Platform.Data.Doublets.Sequences](https://github.com/linksplatform/Data.Doublets.Sequences). + Операции: создание, чтение всех, чтение по id и удаление. Каждое хранилище + Дуплетов проверяется с кэшем последовательностей строк и без него + (`Cached`/`Uncached`). + +Хранилища: SQLite в памяти и в файле; Дуплеты объединённые (один массив связей +с деревьями индексов) и разделённые (отдельные массивы данных и индексов), +каждое энергозависимое (в памяти) и энергонезависимое (в отображаемых в память +файлах). Дуплеты сравниваются с SQLite той же надёжности хранения: +энергозависимые с `SQLite Memory`, энергонезависимые с `SQLite File`. + +Каждый повтор выполняется на новом хранилище в пустой папке после +отбрасываемых прогревочных повторов на не более чем 10 000 записей, которые +длятся не меньше секунды, чтобы JIT .NET уже оптимизировал код. Каждая +операция — одна измеряемая транзакция по всем записям, точечные операции +обходят записи в перемешанном порядке, а каждый результат сверяется +с ожидаемым количеством и зависящей от порядка контрольной суммой, поэтому +хранилище, которое теряет, дублирует или путает записи, завершает запуск +ошибкой, а не попадает в отчёт как быстрое. Число повторов — +`3 000 000 / размер` для связей и `500 000 / размер` для объектов, но от 1 +до 10. В таблицах указано медианное время одной операции; разница указывается, +только если межквартильные диапазоны (средняя половина) повторов +не пересекаются и медианы отличаются больше чем на 5%, иначе пишется +`≈ так же`. Размер файлов измеряется после создания; файлы Дуплетов — заранее +выделенные отображаемые в память файлы, поэтому для небольших хранилищ показан +размер предварительного выделения. + +Каждая таблица измеряется отдельной задачей GitHub Actions со всеми вариантами +на одной машине, чтобы разница никогда не была разницей между машинами, +в [workflow Benchmarks](.github/workflows/benchmarks.yml): связи на 100 000, +1 000 000 и 10 000 000 записей, объекты на 100 000 и 1 000 000 записей. Это +наибольшие размеры, которые укладываются в 6-часовое ограничение задачи: один +повтор SQLite File на 100 000 000 связей не закончился за 4,5 часа +([запуск 37228604862](https://github.com/linksplatform/Comparisons.SQLiteVSDoublets/actions/runs/37228604862)), +а 1 000 000 записей блога на C# уже занимают час. Pull request проверяют весь +процесс на 1 000 записей, а push в `main` обновляет результаты ниже. Workflow +можно запустить и вручную с другими размерами. + +Локальный запуск: -Основано на примерах из https://github.com/FahaoTang/dotnetcore-examples и https://github.com/Konard/LinksPlatform +```bash +mkdir -p results +cargo run --release --manifest-path rust/Cargo.toml -- \ + links 64 100000 --output results/links-rust-64-100000.json +dotnet run -c Release --project csharp/SQLiteVSDoublets -- \ + objects 32 1000 --variants SQLite_File,Doublets_Split_NonVolatile_Cached +# печатает таблицы, для обновления этого файла добавьте +# --readme README.ru.md --charts docs/benchmarks +python3 scripts/benchmark_report.py results +``` -## Автоматизированный набор тестов производительности +Замечания: + +- Разделённые хранилища doublets 0.5.0 (Rust) теряют связь, обновлённую так, + чтобы она ссылалась на саму себя, поэтому тест сначала обновляет связь + до `(0, 0)` — состояния, в котором связи создаются + ([experiments/split_store_delete](experiments/split_store_delete)). +- Хранилища doublets 0.5.0 (Rust) принимают часть памяти, которую + `platform-mem` 0.3.0 возвращает после её увеличения, за всю память, поэтому + хранилище ломается после 1 040 384 связей; тесты оборачивают память + в [`memory::Whole`](rust/src/memory.rs), которая возвращает всю память + ([experiments/unit_store_growth](experiments/unit_store_growth)). +- В C# связи удаляются через `Delete(id, handler: null)`, который сбрасывает + связь перед удалением; простой `Delete(id)` оставляет связь в деревьях + индексов, и последующий поиск ломается + ([experiments/csharp_tree_delete](experiments/csharp_tree_delete)). +- Объединённые хранилища на C# используют АВЛ-деревья индексов: деревья + по умолчанию, сбалансированные по размеру, вырождаются, когда много связей + имеют общее начало или конец, и создание каждой записи блога становится + линейным по числу записей + ([experiments/csharp_objects_profile](experiments/csharp_objects_profile)). +- Раздельные хранилища на C# оставляют связный список внутренних начал, + который включён по умолчанию и которого нет в doublets 0.5.0 (Rust); с ним + обновления на C# быстрее, а остальные операции занимают примерно то же время + ([experiments/csharp_split_linked_list](experiments/csharp_split_linked_list)). +- Хранилища объектов используют внешние ссылки для чисел и символов Юникода, + поэтому исходные значения никогда не совпадают с идентификаторами связей. +- Чтение строки без кэша обходит её последовательность связь за связью; в C# + каждый вызов `GetSource`/`GetTarget` из `Platform.Data` выделяет обработчик + и список, поэтому чтение без кэша на C# значительно медленнее, чем на Rust. + + + +## Дуплеты против SQLite как хранилище связей + +### Дуплеты на Rust против SQLite + +#### Тесты с 32-битным пространством адресов/идентификаторов + +##### 100,000 связей + +_10 повторов после прогрева, медианное время одной операции. SQLite 3.53.2, Ubuntu 24.04.5 LTS, AMD EPYC 9V74 80-Core Processor, 4 cores, 16 GiB, [запуск GitHub Actions](https://github.com/linksplatform/Comparisons.SQLiteVSDoublets/actions/runs/37226110636) от 2026-10-04._ + +| Хранилище | Создание | Чтение всех | Чтение по id | Поиск (from, to) | Чтение по from | Чтение по to | Обновление | Удаление | Размер файлов | +| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | +| SQLite Memory | 1.72 µs | 125 ns | 606 ns | 693 ns | 765 ns | 766 ns | 3.36 µs | 2.17 µs | — | +| SQLite File | 2.09 µs | 128 ns | 621 ns | 727 ns | 782 ns | 790 ns | 12.3 µs | 6.04 µs | 4.4 MiB | +| Doublets United Volatile | 458 ns (в 3.76× быстрее) | 1.91 ns (в 65.7× быстрее) | 5.72 ns (в 106× быстрее) | 194 ns (в 3.57× быстрее) | 248 ns (в 3.09× быстрее) | 267 ns (в 2.86× быстрее) | 1.24 µs (в 2.72× быстрее) | 459 ns (в 4.72× быстрее) | — | +| Doublets United NonVolatile | 499 ns (в 4.19× быстрее) | 1.91 ns (в 67.3× быстрее) | 5.88 ns (в 106× быстрее) | 214 ns (в 3.39× быстрее) | 278 ns (в 2.81× быстрее) | 288 ns (в 2.75× быстрее) | 1.29 µs (в 9.52× быстрее) | 484 ns (в 12.5× быстрее) | 32.0 MiB | +| Doublets Split Volatile | 97.1 ns (в 17.7× быстрее) | 3.68 ns (в 34× быстрее) | 5.56 ns (в 109× быстрее) | 58.7 ns (в 11.8× быстрее) | 29.4 ns (в 26× быстрее) | 30.1 ns (в 25.5× быстрее) | 174 ns (в 19.3× быстрее) | 607 ns (в 3.57× быстрее) | — | +| Doublets Split NonVolatile | 119 ns (в 17.6× быстрее) | 3.57 ns (в 36× быстрее) | 7.43 ns (в 83.6× быстрее) | 61.8 ns (в 11.8× быстрее) | 31.2 ns (в 25.1× быстрее) | 32 ns (в 24.7× быстрее) | 184 ns (в 66.8× быстрее) | 649 ns (в 9.31× быстрее) | 40.0 MiB | + +##### 1,000,000 связей + +_3 повтора после прогрева, медианное время одной операции. SQLite 3.53.2, Ubuntu 24.04.5 LTS, AMD EPYC 7763 64-Core Processor, 4 cores, 16 GiB, [запуск GitHub Actions](https://github.com/linksplatform/Comparisons.SQLiteVSDoublets/actions/runs/37226110636) от 2026-10-04._ + +| Хранилище | Создание | Чтение всех | Чтение по id | Поиск (from, to) | Чтение по from | Чтение по to | Обновление | Удаление | Размер файлов | +| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | +| SQLite Memory | 2.39 µs | 124 ns | 874 ns | 983 ns | 997 ns | 1 µs | 6.48 µs | 4.26 µs | — | +| SQLite File | 5.76 µs | 128 ns | 1.96 µs | 1.9 µs | 1.92 µs | 1.91 µs | 19 µs | 10.6 µs | 48.2 MiB | +| Doublets United Volatile | 755 ns (в 3.16× быстрее) | 1.84 ns (в 67.5× быстрее) | 20.7 ns (в 42.2× быстрее) | 417 ns (в 2.36× быстрее) | 579 ns (в 1.72× быстрее) | 620 ns (в 1.61× быстрее) | 2.39 µs (в 2.71× быстрее) | 828 ns (в 5.14× быстрее) | — | +| Doublets United NonVolatile | 868 ns (в 6.64× быстрее) | 2.11 ns (в 60.5× быстрее) | 22.7 ns (в 86.3× быстрее) | 502 ns (в 3.79× быстрее) | 681 ns (в 2.82× быстрее) | 689 ns (в 2.77× быстрее) | 2.62 µs (в 7.27× быстрее) | 914 ns (в 11.6× быстрее) | 32.0 MiB | +| Doublets Split Volatile | 162 ns (в 14.7× быстрее) | 3.87 ns (в 32.1× быстрее) | 23 ns (в 38.1× быстрее) | 159 ns (в 6.2× быстрее) | 116 ns (в 8.58× быстрее) | 112 ns (в 8.96× быстрее) | 457 ns (в 14.2× быстрее) | 1.2 µs (в 3.55× быстрее) | — | +| Doublets Split NonVolatile | 174 ns (в 33.1× быстрее) | 3.91 ns (в 32.7× быстрее) | 24.9 ns (в 78.6× быстрее) | 167 ns (в 11.4× быстрее) | 129 ns (в 14.9× быстрее) | 124 ns (в 15.4× быстрее) | 494 ns (в 38.6× быстрее) | 1.42 µs (в 7.5× быстрее) | 40.0 MiB | + +##### 10,000,000 связей + +_1 повтор после прогрева, медианное время одной операции. SQLite 3.53.2, Ubuntu 24.04.5 LTS, AMD EPYC 7763 64-Core Processor, 4 cores, 16 GiB, [запуск GitHub Actions](https://github.com/linksplatform/Comparisons.SQLiteVSDoublets/actions/runs/37226110636) от 2026-10-04._ + +| Хранилище | Создание | Чтение всех | Чтение по id | Поиск (from, to) | Чтение по from | Чтение по to | Обновление | Удаление | Размер файлов | +| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | +| SQLite Memory | 3.34 µs | 124 ns | 1.39 µs | 1.47 µs | 1.51 µs | 1.49 µs | 8.53 µs | 5.59 µs | — | +| SQLite File | 9.22 µs | 127 ns | 2.18 µs | 2.57 µs | 2.56 µs | 2.53 µs | 24 µs | 14.5 µs | 501.0 MiB | +| Doublets United Volatile | 2.16 µs (в 1.55× быстрее) | 2.26 ns (в 54.6× быстрее) | 35 ns (в 39.7× быстрее) | 1.26 µs (в 1.17× быстрее) | 1.56 µs (≈ так же) | 1.61 µs (в 1.08× медленнее) | 6.55 µs (в 1.3× быстрее) | 2.73 µs (в 2.05× быстрее) | — | +| Doublets United NonVolatile | 2.42 µs (в 3.81× быстрее) | 2.36 ns (в 53.5× быстрее) | 32.6 ns (в 66.8× быстрее) | 1.27 µs (в 2.02× быстрее) | 1.52 µs (в 1.68× быстрее) | 1.56 µs (в 1.62× быстрее) | 6.91 µs (в 3.48× быстрее) | 2.96 µs (в 4.89× быстрее) | 320.0 MiB | +| Doublets Split Volatile | 317 ns (в 10.5× быстрее) | 3.88 ns (в 31.9× быстрее) | 30.1 ns (в 46.2× быстрее) | 240 ns (в 6.14× быстрее) | 170 ns (в 8.84× быстрее) | 169 ns (в 8.82× быстрее) | 752 ns (в 11.3× быстрее) | 2.64 µs (в 2.12× быстрее) | — | +| Doublets Split NonVolatile | 467 ns (в 19.8× быстрее) | 3.94 ns (в 32.1× быстрее) | 34.7 ns (в 62.7× быстрее) | 251 ns (в 10.2× быстрее) | 181 ns (в 14.1× быстрее) | 187 ns (в 13.5× быстрее) | 781 ns (в 30.8× быстрее) | 2.92 µs (в 4.96× быстрее) | 400.0 MiB | + +![Дуплеты на Rust против SQLite, 32 бит, связи](docs/benchmarks/links-rust-32.png) + +#### Тесты с 64-битным пространством адресов/идентификаторов + +##### 100,000 связей + +_10 повторов после прогрева, медианное время одной операции. SQLite 3.53.2, Ubuntu 24.04.5 LTS, AMD EPYC 7763 64-Core Processor, 4 cores, 16 GiB, [запуск GitHub Actions](https://github.com/linksplatform/Comparisons.SQLiteVSDoublets/actions/runs/37226110636) от 2026-10-04._ + +| Хранилище | Создание | Чтение всех | Чтение по id | Поиск (from, to) | Чтение по from | Чтение по to | Обновление | Удаление | Размер файлов | +| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | +| SQLite Memory | 1.69 µs | 126 ns | 616 ns | 721 ns | 752 ns | 743 ns | 3.12 µs | 2.01 µs | — | +| SQLite File | 2.02 µs | 130 ns | 639 ns | 762 ns | 789 ns | 788 ns | 10.4 µs | 5.18 µs | 4.4 MiB | +| Doublets United Volatile | 449 ns (в 3.77× быстрее) | 1.81 ns (в 69.8× быстрее) | 3.2 ns (в 192× быстрее) | 179 ns (в 4.04× быстрее) | 234 ns (в 3.21× быстрее) | 253 ns (в 2.94× быстрее) | 1.13 µs (в 2.75× быстрее) | 444 ns (в 4.54× быстрее) | — | +| Doublets United NonVolatile | 471 ns (в 4.29× быстрее) | 1.89 ns (в 68.9× быстрее) | 3.52 ns (в 182× быстрее) | 198 ns (в 3.85× быстрее) | 255 ns (в 3.1× быстрее) | 269 ns (в 2.93× быстрее) | 1.19 µs (в 8.73× быстрее) | 462 ns (в 11.2× быстрее) | 64.0 MiB | +| Doublets Split Volatile | 114 ns (в 14.8× быстрее) | 3.77 ns (в 33.5× быстрее) | 4.12 ns (в 149× быстрее) | 56.1 ns (в 12.9× быстрее) | 27.6 ns (в 27.2× быстрее) | 29.2 ns (в 25.4× быстрее) | 164 ns (в 19× быстрее) | 591 ns (в 3.41× быстрее) | — | +| Doublets Split NonVolatile | 106 ns (в 19× быстрее) | 3.74 ns (в 34.8× быстрее) | 3.91 ns (в 164× быстрее) | 57 ns (в 13.4× быстрее) | 28.7 ns (в 27.5× быстрее) | 29.9 ns (в 26.4× быстрее) | 171 ns (в 61× быстрее) | 609 ns (в 8.49× быстрее) | 80.0 MiB | + +##### 1,000,000 связей + +_3 повтора после прогрева, медианное время одной операции. SQLite 3.53.2, Ubuntu 24.04.5 LTS, AMD EPYC 7763 64-Core Processor, 4 cores, 16 GiB, [запуск GitHub Actions](https://github.com/linksplatform/Comparisons.SQLiteVSDoublets/actions/runs/37226110636) от 2026-10-04._ + +| Хранилище | Создание | Чтение всех | Чтение по id | Поиск (from, to) | Чтение по from | Чтение по to | Обновление | Удаление | Размер файлов | +| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | +| SQLite Memory | 2.09 µs | 124 ns | 745 ns | 839 ns | 879 ns | 858 ns | 4.77 µs | 3.14 µs | — | +| SQLite File | 5.65 µs | 127 ns | 1.83 µs | 1.82 µs | 1.85 µs | 1.84 µs | 18.4 µs | 10.3 µs | 48.2 MiB | +| Doublets United Volatile | 750 ns (в 2.79× быстрее) | 3.05 ns (в 40.4× быстрее) | 20.2 ns (в 36.9× быстрее) | 385 ns (в 2.18× быстрее) | 471 ns (в 1.87× быстрее) | 567 ns (в 1.51× быстрее) | 2.55 µs (в 1.87× быстрее) | 856 ns (в 3.66× быстрее) | — | +| Doublets United NonVolatile | 840 ns (в 6.72× быстрее) | 3.5 ns (в 36.2× быстрее) | 22.7 ns (в 80.7× быстрее) | 495 ns (в 3.67× быстрее) | 622 ns (в 2.97× быстрее) | 642 ns (в 2.87× быстрее) | 2.91 µs (в 6.31× быстрее) | 1.02 µs (в 10× быстрее) | 64.0 MiB | +| Doublets Split Volatile | 184 ns (в 11.4× быстрее) | 4.05 ns (в 30.5× быстрее) | 21.6 ns (в 34.5× быстрее) | 171 ns (в 4.9× быстрее) | 130 ns (в 6.76× быстрее) | 132 ns (в 6.5× быстрее) | 485 ns (в 9.83× быстрее) | 1.18 µs (в 2.65× быстрее) | — | +| Doublets Split NonVolatile | 190 ns (в 29.8× быстрее) | 4.4 ns (в 28.8× быстрее) | 23.8 ns (в 76.9× быстрее) | 181 ns (в 10.1× быстрее) | 141 ns (в 13.1× быстрее) | 141 ns (в 13.1× быстрее) | 532 ns (в 34.5× быстрее) | 1.38 µs (в 7.43× быстрее) | 80.0 MiB | + +##### 10,000,000 связей + +_1 повтор после прогрева, медианное время одной операции. SQLite 3.53.2, Ubuntu 24.04.5 LTS, INTEL(R) XEON(R) PLATINUM 8573C, 4 cores, 16 GiB, [запуск GitHub Actions](https://github.com/linksplatform/Comparisons.SQLiteVSDoublets/actions/runs/37226110636) от 2026-10-04._ + +| Хранилище | Создание | Чтение всех | Чтение по id | Поиск (from, to) | Чтение по from | Чтение по to | Обновление | Удаление | Размер файлов | +| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | +| SQLite Memory | 3.34 µs | 105 ns | 1.62 µs | 1.62 µs | 1.57 µs | 1.5 µs | 9.58 µs | 6.3 µs | — | +| SQLite File | 7.41 µs | 106 ns | 1.81 µs | 2.05 µs | 1.98 µs | 1.97 µs | 18.3 µs | 10.9 µs | 501.0 MiB | +| Doublets United Volatile | 2.38 µs (в 1.41× быстрее) | 6.42 ns (в 16.4× быстрее) | 22.4 ns (в 72.3× быстрее) | 1.3 µs (в 1.25× быстрее) | 1.43 µs (в 1.1× быстрее) | 2.3 µs (в 1.53× медленнее) | 6.59 µs (в 1.45× быстрее) | 2.48 µs (в 2.54× быстрее) | — | +| Doublets United NonVolatile | 2.72 µs (в 2.72× быстрее) | 6.31 ns (в 16.9× быстрее) | 22.4 ns (в 81× быстрее) | 1.42 µs (в 1.44× быстрее) | 1.67 µs (в 1.19× быстрее) | 1.65 µs (в 1.19× быстрее) | 7.75 µs (в 2.36× быстрее) | 3.36 µs (в 3.25× быстрее) | 640.0 MiB | +| Doublets Split Volatile | 363 ns (в 9.19× быстрее) | 6.9 ns (в 15.3× быстрее) | 23.4 ns (в 69.3× быстрее) | 246 ns (в 6.59× быстрее) | 141 ns (в 11.1× быстрее) | 148 ns (в 10.1× быстрее) | 728 ns (в 13.2× быстрее) | 3.05 µs (в 2.07× быстрее) | — | +| Doublets Split NonVolatile | 644 ns (в 11.5× быстрее) | 7.01 ns (в 15.2× быстрее) | 26.2 ns (в 69.2× быстрее) | 240 ns (в 8.57× быстрее) | 143 ns (в 13.9× быстрее) | 145 ns (в 13.6× быстрее) | 709 ns (в 25.8× быстрее) | 3.62 µs (в 3.01× быстрее) | 800.0 MiB | -Одинаковая нагрузка со связями выполняется для SQLite в памяти и в файле, а -также для четырёх вариантов Дуплетов: объединённого/разделённого и -энергозависимого/энергонезависимого. Измеряются создание, обновление, удаление, -перечисление всех связей и запросы по идентификатору, конкретной паре -`(начало, конец)`, началу и концу. Набор на Rust также измеряет создание, -чтение и удаление объектоподобных записей блога во всех вариантах хранилищ. +![Дуплеты на Rust против SQLite, 64 бит, связи](docs/benchmarks/links-rust-64.png) -Подготовка и очистка данных не входят в измеряемый интервал. Для pull request -запускается сокращённая проверка, а исходный вывод, таблицы и диаграммы -сохраняются как артефакты workflow. Полные запуски в `main` публикуют таблицы и -диаграммы ниже вместе со ссылкой на создавший их запуск. +### Дуплеты на C# против SQLite -Локальный запуск проверок: +#### Тесты с 32-битным пространством адресов/идентификаторов -```bash -cd rust -cargo test --lib --tests -cargo check --benches -BENCHMARK_LINK_COUNT=10 BACKGROUND_LINK_COUNT=30 \ - BENCHMARK_OBJECT_COUNT=5 cargo bench --bench bench -- \ - --output-format bencher - -cd ../csharp -dotnet run -c Release -- --self-test -BENCHMARK_LINK_COUNT=10 BACKGROUND_LINK_COUNT=30 \ - dotnet run -c Release -- --filter '*LinksBenchmarks*' -``` +##### 100,000 связей + +_10 повторов после прогрева, медианное время одной операции. SQLite 3.53.3, Ubuntu 24.04.5 LTS, AMD EPYC 7763 64-Core Processor, 4 cores, 16 GiB, [запуск GitHub Actions](https://github.com/linksplatform/Comparisons.SQLiteVSDoublets/actions/runs/37226110636) от 2026-10-04._ + +| Хранилище | Создание | Чтение всех | Чтение по id | Поиск (from, to) | Чтение по from | Чтение по to | Обновление | Удаление | Размер файлов | +| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | +| SQLite Memory | 6.27 µs | 380 ns | 1.2 µs | 1.46 µs | 1.51 µs | 1.5 µs | 4.27 µs | 2.84 µs | — | +| SQLite File | 5.75 µs | 385 ns | 1.21 µs | 1.48 µs | 1.52 µs | 1.52 µs | 12.2 µs | 6.22 µs | 4.4 MiB | +| Doublets United Volatile | 1.02 µs (в 6.12× быстрее) | 14.9 ns (в 25.5× быстрее) | 60.1 ns (в 20× быстрее) | 226 ns (в 6.45× быстрее) | 350 ns (в 4.32× быстрее) | 264 ns (в 5.67× быстрее) | 2.03 µs (в 2.1× быстрее) | 1.05 µs (в 2.7× быстрее) | — | +| Doublets United NonVolatile | 1.08 µs (в 5.32× быстрее) | 14.8 ns (в 26× быстрее) | 60.5 ns (в 20× быстрее) | 258 ns (в 5.73× быстрее) | 366 ns (в 4.17× быстрее) | 267 ns (в 5.68× быстрее) | 2.11 µs (в 5.76× быстрее) | 1.06 µs (в 5.89× быстрее) | 32.0 MiB | +| Doublets Split Volatile | 192 ns (в 32.7× быстрее) | 16 ns (в 23.7× быстрее) | 69.6 ns (в 17.2× быстрее) | 107 ns (в 13.7× быстрее) | 84.5 ns (в 17.9× быстрее) | 84.8 ns (в 17.7× быстрее) | 218 ns (в 19.6× быстрее) | 276 ns (в 10.3× быстрее) | — | +| Doublets Split NonVolatile | 241 ns (в 23.9× быстрее) | 16.5 ns (в 23.4× быстрее) | 69.1 ns (в 17.5× быстрее) | 110 ns (в 13.5× быстрее) | 86.7 ns (в 17.6× быстрее) | 86.4 ns (в 17.6× быстрее) | 225 ns (в 54.1× быстрее) | 263 ns (в 23.6× быстрее) | 40.0 MiB | + +##### 1,000,000 связей + +_3 повтора после прогрева, медианное время одной операции. SQLite 3.53.3, Ubuntu 24.04.5 LTS, Intel(R) Xeon(R) 6973P-C, 4 cores, 16 GiB, [запуск GitHub Actions](https://github.com/linksplatform/Comparisons.SQLiteVSDoublets/actions/runs/37226110636) от 2026-10-04._ -### Результаты операций со связями в C#-версии +| Хранилище | Создание | Чтение всех | Чтение по id | Поиск (from, to) | Чтение по from | Чтение по to | Обновление | Удаление | Размер файлов | +| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | +| SQLite Memory | 7.05 µs | 428 ns | 1.51 µs | 1.69 µs | 1.76 µs | 1.77 µs | 6.76 µs | 4.12 µs | — | +| SQLite File | 6.89 µs | 385 ns | 1.81 µs | 1.95 µs | 2.02 µs | 2.04 µs | 12.3 µs | 8.07 µs | 48.2 MiB | +| Doublets United Volatile | 1.7 µs (в 4.16× быстрее) | 16.4 ns (в 26.1× быстрее) | 117 ns (в 12.9× быстрее) | 574 ns (в 2.94× быстрее) | 691 ns (в 2.55× быстрее) | 824 ns (в 2.15× быстрее) | 4.3 µs (в 1.57× быстрее) | 1.85 µs (в 2.23× быстрее) | — | +| Doublets United NonVolatile | 1.85 µs (в 3.72× быстрее) | 16.5 ns (в 23.4× быстрее) | 123 ns (в 14.7× быстрее) | 688 ns (в 2.83× быстрее) | 861 ns (в 2.35× быстрее) | 982 ns (в 2.07× быстрее) | 4.93 µs (в 2.49× быстрее) | 2.25 µs (в 3.59× быстрее) | 32.0 MiB | +| Doublets Split Volatile | 400 ns (в 17.6× быстрее) | 16.8 ns (в 25.5× быстрее) | 231 ns (в 6.54× быстрее) | 396 ns (в 4.26× быстрее) | 324 ns (в 5.44× быстрее) | 338 ns (в 5.24× быстрее) | 594 ns (в 11.4× быстрее) | 674 ns (в 6.11× быстрее) | — | +| Doublets Split NonVolatile | 467 ns (в 14.8× быстрее) | 17.6 ns (в 21.9× быстрее) | 238 ns (в 7.58× быстрее) | 439 ns (в 4.43× быстрее) | 369 ns (в 5.49× быстрее) | 357 ns (в 5.7× быстрее) | 618 ns (в 19.9× быстрее) | 601 ns (в 13.4× быстрее) | 40.0 MiB | - -_Generated 2026-09-19 18:13 UTC for C# by [GitHub Actions run 35459522587](https://github.com/linksplatform/Comparisons.SQLiteVSDoublets/actions/runs/35459522587) — 1000 benchmarked links, 3000 background links._ +##### 10,000,000 связей -| Operation | Doublets United Volatile | Doublets United NonVolatile | Doublets Split Volatile | Doublets Split NonVolatile | SQLite Memory | SQLite File | -|---------------|--------------------------|-----------------------------|-------------------------|----------------------------|---------------|-------------| -| Create | 2414327 (3.7x faster) | 2512520 (3.5x faster) | 867200 (10.2x faster) | 896999 (9.8x faster) | 8818051 | 2294476668 | -| Update | 4566053 (2.3x faster) | 4670899 (2.3x faster) | 1541871 (7.0x faster) | 1534119 (7.0x faster) | 10720700 | 2733072615 | -| Delete | 484008 (14.3x faster) | 465524 (14.8x faster) | 523910 (13.2x faster) | 521322 (13.3x faster) | 6907869 | 1682008343 | -| Each All | 437356 (3.7x faster) | 470885 (3.4x faster) | 420518 (3.8x faster) | 397666 (4.0x faster) | 2113072 | 1598861 | -| Each Identity | 430962 (11.6x faster) | 411173 (12.1x faster) | 403128 (12.4x faster) | 385531 (13.0x faster) | 4995738 | 12044311 | -| Each Concrete | 1363130 (5.2x faster) | 1408600 (5.1x faster) | 647769 (11.0x faster) | 917509 (7.8x faster) | 7134655 | 13622920 | -| Each Outgoing | 1160157 (5.3x faster) | 1180175 (5.3x faster) | 862790 (7.2x faster) | 830070 (7.5x faster) | 6199124 | 12923172 | -| Each Incoming | 1139559 (5.7x faster) | 1169325 (5.6x faster) | 796694 (8.2x faster) | 776158 (8.4x faster) | 6533836 | 13510164 | +_1 повтор после прогрева, медианное время одной операции. SQLite 3.53.3, Ubuntu 24.04.5 LTS, AMD EPYC 9V74 80-Core Processor, 4 cores, 16 GiB, [запуск GitHub Actions](https://github.com/linksplatform/Comparisons.SQLiteVSDoublets/actions/runs/37226110636) от 2026-10-04._ -![C# benchmark comparison](docs/benchmarks/bench_csharp.png) +| Хранилище | Создание | Чтение всех | Чтение по id | Поиск (from, to) | Чтение по from | Чтение по to | Обновление | Удаление | Размер файлов | +| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | +| SQLite Memory | 7.41 µs | 316 ns | 1.86 µs | 2.01 µs | 2.06 µs | 2.06 µs | 9.49 µs | 6.49 µs | — | +| SQLite File | 18.2 µs | 318 ns | 2.72 µs | 3.16 µs | 3.11 µs | 3.1 µs | 24.6 µs | 14.8 µs | 501.0 MiB | +| Doublets United Volatile | 3.01 µs (в 2.46× быстрее) | 10 ns (в 31.6× быстрее) | 181 ns (в 10.3× быстрее) | 1.45 µs (в 1.39× быстрее) | 1.67 µs (в 1.23× быстрее) | 1.88 µs (в 1.1× быстрее) | 8.16 µs (в 1.16× быстрее) | 3.52 µs (в 1.85× быстрее) | — | +| Doublets United NonVolatile | 3.95 µs (в 4.61× быстрее) | 14.1 ns (в 22.6× быстрее) | 187 ns (в 14.5× быстрее) | 1.44 µs (в 2.19× быстрее) | 1.64 µs (в 1.9× быстрее) | 1.93 µs (в 1.61× быстрее) | 8.99 µs (в 2.74× быстрее) | 4.35 µs (в 3.39× быстрее) | 320.0 MiB | +| Doublets Split Volatile | 431 ns (в 17.2× быстрее) | 10.3 ns (в 30.8× быстрее) | 199 ns (в 9.3× быстрее) | 475 ns (в 4.23× быстрее) | 323 ns (в 6.38× быстрее) | 324 ns (в 6.37× быстрее) | 795 ns (в 11.9× быстрее) | 574 ns (в 11.3× быстрее) | — | +| Doublets Split NonVolatile | 1.16 µs (в 15.7× быстрее) | 9.1 ns (в 35× быстрее) | 211 ns (в 12.9× быстрее) | 508 ns (в 6.21× быстрее) | 354 ns (в 8.79× быстрее) | 357 ns (в 8.69× быстрее) | 1.01 µs (в 24.5× быстрее) | 780 ns (в 18.9× быстрее) | 400.0 MiB | -![C# benchmark comparison, logarithmic scale](docs/benchmarks/bench_csharp_log_scale.png) - +![Дуплеты на C# против SQLite, 32 бит, связи](docs/benchmarks/links-csharp-32.png) -### Результаты операций со связями и объектами на Rust +#### Тесты с 64-битным пространством адресов/идентификаторов + +##### 100,000 связей - -_Generated 2026-09-19 18:46 UTC for Rust by [GitHub Actions run 35459522519](https://github.com/linksplatform/Comparisons.SQLiteVSDoublets/actions/runs/35459522519) — 1000 benchmarked links, 3000 background links, 1000 objects._ +_10 повторов после прогрева, медианное время одной операции. SQLite 3.53.3, Ubuntu 24.04.5 LTS, Intel(R) Xeon(R) 6973P-C, 4 cores, 16 GiB, [запуск GitHub Actions](https://github.com/linksplatform/Comparisons.SQLiteVSDoublets/actions/runs/37226110636) от 2026-10-04._ -| Operation | Doublets United Volatile | Doublets United NonVolatile | Doublets Split Volatile | Doublets Split NonVolatile | SQLite Memory | SQLite File | -|---------------------|--------------------------|-----------------------------|-------------------------|----------------------------|---------------|-------------| -| Create | 141747 (40.7x faster) | 143659 (40.2x faster) | 69524 (83.0x faster) | 69702 (82.8x faster) | 5772002 | 993908545 | -| Update | 283809 (29.6x faster) | 277912 (30.2x faster) | 63882 (131.3x faster) | 72899 (115.1x faster) | 8390541 | 1067410860 | -| Delete | 168149 (26.8x faster) | 166496 (27.1x faster) | 77711 (58.0x faster) | 78217 (57.6x faster) | 4509130 | 977528491 | -| Each All | 31671 (15.9x faster) | 31638 (15.9x faster) | 34426 (14.6x faster) | 34487 (14.6x faster) | 504031 | 595624 | -| Each Identity | 2132 (1269.3x faster) | 2136 (1266.9x faster) | 2130 (1270.5x faster) | 2127 (1272.3x faster) | 2706180 | 10678850 | -| Each Concrete | 124784 (34.7x faster) | 120544 (36.0x faster) | 45647 (95.0x faster) | 43793 (99.0x faster) | 4335748 | 12668554 | -| Each Outgoing | 210057 (18.1x faster) | 208064 (18.2x faster) | 44112 (86.0x faster) | 45302 (83.7x faster) | 3792959 | 12033519 | -| Each Incoming | 212266 (19.3x faster) | 210393 (19.4x faster) | 50873 (80.4x faster) | 46396 (88.1x faster) | 4089215 | 12477405 | -| Objects Create List | 8759735 (10.5x slower) | 8759735 (10.5x slower) | 5388349 (6.5x slower) | 5436404 (6.5x slower) | 832974 | 2098461 | -| Objects Read List | 5889568 (18.5x slower) | 5585959 (17.6x slower) | 5393697 (17.0x slower) | 4889083 (15.4x slower) | 317868 | 343282 | -| Objects Delete List | 2709133 (75.1x slower) | 2667861 (74.0x slower) | 1083231 (30.0x slower) | 1106546 (30.7x slower) | 36069 | 992614 | +| Хранилище | Создание | Чтение всех | Чтение по id | Поиск (from, to) | Чтение по from | Чтение по to | Обновление | Удаление | Размер файлов | +| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | +| SQLite Memory | 4.63 µs | 355 ns | 882 ns | 1.07 µs | 1.12 µs | 1.13 µs | 3.45 µs | 2.3 µs | — | +| SQLite File | 3.83 µs | 363 ns | 844 ns | 1.03 µs | 1.07 µs | 1.08 µs | 7.17 µs | 4.04 µs | 4.4 MiB | +| Doublets United Volatile | 1.12 µs (в 4.12× быстрее) | 19.6 ns (в 18.1× быстрее) | 65 ns (в 13.6× быстрее) | 223 ns (в 4.81× быстрее) | 265 ns (в 4.23× быстрее) | 371 ns (в 3.04× быстрее) | 2.49 µs (в 1.39× быстрее) | 1.24 µs (в 1.86× быстрее) | — | +| Doublets United NonVolatile | 1.2 µs (в 3.19× быстрее) | 20.8 ns (в 17.4× быстрее) | 73 ns (в 11.6× быстрее) | 279 ns (в 3.68× быстрее) | 360 ns (в 2.96× быстрее) | 299 ns (в 3.61× быстрее) | 2.57 µs (в 2.8× быстрее) | 1.24 µs (в 3.26× быстрее) | 64.0 MiB | +| Doublets Split Volatile | 247 ns (в 18.8× быстрее) | 15.4 ns (в 23.1× быстрее) | 91.2 ns (в 9.67× быстрее) | 130 ns (в 8.23× быстрее) | 118 ns (в 9.54× быстрее) | 113 ns (в 9.98× быстрее) | 234 ns (в 14.8× быстрее) | 296 ns (в 7.76× быстрее) | — | +| Doublets Split NonVolatile | 278 ns (в 13.8× быстрее) | 14 ns (в 26× быстрее) | 86.6 ns (в 9.74× быстрее) | 128 ns (в 8.03× быстрее) | 118 ns (в 9.03× быстрее) | 116 ns (в 9.32× быстрее) | 236 ns (в 30.4× быстрее) | 266 ns (в 15.2× быстрее) | 80.0 MiB | + +##### 1,000,000 связей + +_3 повтора после прогрева, медианное время одной операции. SQLite 3.53.3, Ubuntu 24.04.5 LTS, AMD EPYC 7763 64-Core Processor, 4 cores, 16 GiB, [запуск GitHub Actions](https://github.com/linksplatform/Comparisons.SQLiteVSDoublets/actions/runs/37226110636) от 2026-10-04._ + +| Хранилище | Создание | Чтение всех | Чтение по id | Поиск (from, to) | Чтение по from | Чтение по to | Обновление | Удаление | Размер файлов | +| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | +| SQLite Memory | 7.39 µs | 386 ns | 1.49 µs | 1.8 µs | 1.83 µs | 1.83 µs | 7.12 µs | 4.86 µs | — | +| SQLite File | 9.97 µs | 386 ns | 2.64 µs | 2.76 µs | 2.84 µs | 2.86 µs | 21 µs | 11.7 µs | 48.2 MiB | +| Doublets United Volatile | 2.15 µs (в 3.44× быстрее) | 22.3 ns (в 17.3× быстрее) | 162 ns (в 9.17× быстрее) | 576 ns (в 3.12× быстрее) | 687 ns (в 2.66× быстрее) | 918 ns (в 2× быстрее) | 5.14 µs (в 1.39× быстрее) | 2.32 µs (в 2.1× быстрее) | — | +| Doublets United NonVolatile | 2.2 µs (в 4.54× быстрее) | 21.6 ns (в 17.8× быстрее) | 169 ns (в 15.6× быстрее) | 710 ns (в 3.89× быстрее) | 826 ns (в 3.44× быстрее) | 1.01 µs (в 2.83× быстрее) | 5.66 µs (в 3.7× быстрее) | 2.58 µs (в 4.55× быстрее) | 64.0 MiB | +| Doublets Split Volatile | 366 ns (в 20.2× быстрее) | 20.8 ns (в 18.6× быстрее) | 220 ns (в 6.75× быстрее) | 328 ns (в 5.49× быстрее) | 280 ns (в 6.51× быстрее) | 282 ns (в 6.51× быстрее) | 658 ns (в 10.8× быстрее) | 567 ns (в 8.57× быстрее) | — | +| Doublets Split NonVolatile | 458 ns (в 21.8× быстрее) | 21.2 ns (в 18.2× быстрее) | 245 ns (в 10.8× быстрее) | 369 ns (в 7.48× быстрее) | 317 ns (в 8.97× быстрее) | 323 ns (в 8.84× быстрее) | 716 ns (в 29.3× быстрее) | 580 ns (в 20.2× быстрее) | 80.0 MiB | -![Rust benchmark comparison](docs/benchmarks/bench_rust.png) +##### 10,000,000 связей -![Rust benchmark comparison, logarithmic scale](docs/benchmarks/bench_rust_log_scale.png) - +_1 повтор после прогрева, медианное время одной операции. SQLite 3.53.3, Ubuntu 24.04.5 LTS, AMD EPYC 7763 64-Core Processor, 4 cores, 16 GiB, [запуск GitHub Actions](https://github.com/linksplatform/Comparisons.SQLiteVSDoublets/actions/runs/37226110636) от 2026-10-04._ -Исходное сравнение объектов на C# и его исторические результаты сохранены ниже. +| Хранилище | Создание | Чтение всех | Чтение по id | Поиск (from, to) | Чтение по from | Чтение по to | Обновление | Удаление | Размер файлов | +| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | +| SQLite Memory | 8.4 µs | 384 ns | 1.89 µs | 2.21 µs | 2.18 µs | 2.16 µs | 9.45 µs | 6.63 µs | — | +| SQLite File | 20 µs | 383 ns | 2.88 µs | 3.38 µs | 3.32 µs | 3.32 µs | 25.4 µs | 15.3 µs | 501.0 MiB | +| Doublets United Volatile | 3.04 µs (в 2.76× быстрее) | 13.9 ns (в 27.7× быстрее) | 180 ns (в 10.5× быстрее) | 1.31 µs (в 1.69× быстрее) | 1.5 µs (в 1.45× быстрее) | 1.75 µs (в 1.24× быстрее) | 8.6 µs (в 1.1× быстрее) | 3.84 µs (в 1.72× быстрее) | — | +| Doublets United NonVolatile | 4.43 µs (в 4.52× быстрее) | 21.6 ns (в 17.7× быстрее) | 172 ns (в 16.8× быстрее) | 1.36 µs (в 2.49× быстрее) | 1.55 µs (в 2.14× быстрее) | 1.65 µs (в 2.01× быстрее) | 9.64 µs (в 2.63× быстрее) | 4.78 µs (в 3.19× быстрее) | 640.0 MiB | +| Doublets Split Volatile | 458 ns (в 18.4× быстрее) | 13 ns (в 29.5× быстрее) | 308 ns (в 6.12× быстрее) | 445 ns (в 4.97× быстрее) | 329 ns (в 6.64× быстрее) | 350 ns (в 6.17× быстрее) | 908 ns (в 10.4× быстрее) | 725 ns (в 9.14× быстрее) | — | +| Doublets Split NonVolatile | 1.26 µs (в 15.8× быстрее) | 11.9 ns (в 32.3× быстрее) | 326 ns (в 8.84× быстрее) | 467 ns (в 7.24× быстрее) | 332 ns (в 10× быстрее) | 345 ns (в 9.64× быстрее) | 1.05 µs (в 24.2× быстрее) | 875 ns (в 17.4× быстрее) | 800.0 MiB | + +![Дуплеты на C# против SQLite, 64 бит, связи](docs/benchmarks/links-csharp-64.png) + +## Дуплеты против SQLite как хранилище объектов + +### Дуплеты на Rust против SQLite + +#### Тесты с 32-битным пространством адресов/идентификаторов + +##### 100,000 записей блога + +_5 повторов после прогрева, медианное время одной операции. SQLite 3.53.2, Ubuntu 24.04.5 LTS, Intel(R) Xeon(R) 6973P-C, 4 cores, 16 GiB, [запуск GitHub Actions](https://github.com/linksplatform/Comparisons.SQLiteVSDoublets/actions/runs/37226110636) от 2026-10-04._ + +| Хранилище | Создание | Чтение всех | Чтение по id | Удаление | Размер файлов | +| --- | ---: | ---: | ---: | ---: | ---: | +| SQLite Memory | 838 ns | 485 ns | 1.37 µs | 1.49 µs | — | +| SQLite File | 4.96 µs | 476 ns | 1.63 µs | 8.03 µs | 78.3 MiB | +| Doublets United Volatile Cached | 4.58 µs (в 5.46× медленнее) | 1.35 µs (в 2.79× медленнее) | 2 µs (в 1.46× медленнее) | 4.51 µs (в 3.03× медленнее) | — | +| Doublets United Volatile Uncached | 95.5 µs (в 114× медленнее) | 7.59 µs (в 15.7× медленнее) | 8.21 µs (в 5.99× медленнее) | 4.52 µs (в 3.04× медленнее) | — | +| Doublets United NonVolatile Cached | 4.87 µs (≈ так же) | 1.41 µs (в 2.96× медленнее) | 2.25 µs (в 1.38× медленнее) | 4.82 µs (в 1.66× быстрее) | 32.0 MiB | +| Doublets United NonVolatile Uncached | 92.7 µs (в 18.7× медленнее) | 7.8 µs (в 16.4× медленнее) | 8.54 µs (в 5.24× медленнее) | 4.86 µs (в 1.65× быстрее) | 32.0 MiB | +| Doublets Split Volatile Cached | 2.89 µs (в 3.45× медленнее) | 698 ns (в 1.44× медленнее) | 601 ns (в 2.28× быстрее) | 2.18 µs (в 1.47× медленнее) | — | +| Doublets Split Volatile Uncached | 36.3 µs (в 43.3× медленнее) | 9.78 µs (в 20.2× медленнее) | 10.1 µs (в 7.36× медленнее) | 2.11 µs (в 1.42× медленнее) | — | +| Doublets Split NonVolatile Cached | 2.84 µs (в 1.75× быстрее) | 641 ns (в 1.35× медленнее) | 588 ns (в 2.77× быстрее) | 2.33 µs (в 3.45× быстрее) | 40.0 MiB | +| Doublets Split NonVolatile Uncached | 35.9 µs (в 7.23× медленнее) | 9.33 µs (в 19.6× медленнее) | 9.69 µs (в 5.94× медленнее) | 2.37 µs (в 3.39× быстрее) | 40.0 MiB | + +##### 1,000,000 записей блога + +_1 повтор после прогрева, медианное время одной операции. SQLite 3.53.2, Ubuntu 24.04.5 LTS, INTEL(R) XEON(R) PLATINUM 8573C, 4 cores, 16 GiB, [запуск GitHub Actions](https://github.com/linksplatform/Comparisons.SQLiteVSDoublets/actions/runs/37226110636) от 2026-10-04._ + +| Хранилище | Создание | Чтение всех | Чтение по id | Удаление | Размер файлов | +| --- | ---: | ---: | ---: | ---: | ---: | +| SQLite Memory | 965 ns | 478 ns | 2.06 µs | 2.7 µs | — | +| SQLite File | 5.93 µs | 470 ns | 2.71 µs | 11.1 µs | 783.2 MiB | +| Doublets United Volatile Cached | 6.88 µs (в 7.13× медленнее) | 1.64 µs (в 3.42× медленнее) | 3.23 µs (в 1.56× медленнее) | 10.7 µs (в 3.97× медленнее) | — | +| Doublets United Volatile Uncached | 113 µs (в 117× медленнее) | 8.34 µs (в 17.4× медленнее) | 10.3 µs (в 5× медленнее) | 9.72 µs (в 3.6× медленнее) | — | +| Doublets United NonVolatile Cached | 7.85 µs (в 1.32× медленнее) | 1.61 µs (в 3.43× медленнее) | 3.15 µs (в 1.16× медленнее) | 10.4 µs (в 1.06× быстрее) | 320.0 MiB | +| Doublets United NonVolatile Uncached | 121 µs (в 20.3× медленнее) | 8.46 µs (в 18× медленнее) | 10.4 µs (в 3.83× медленнее) | 10.4 µs (в 1.06× быстрее) | 320.0 MiB | +| Doublets Split Volatile Cached | 4.16 µs (в 4.32× медленнее) | 914 ns (в 1.91× медленнее) | 1.06 µs (в 1.95× быстрее) | 4.99 µs (в 1.85× медленнее) | — | +| Doublets Split Volatile Uncached | 39.3 µs (в 40.7× медленнее) | 11.2 µs (в 23.4× медленнее) | 11.7 µs (в 5.69× медленнее) | 4.8 µs (в 1.78× медленнее) | — | +| Doublets Split NonVolatile Cached | 5.52 µs (в 1.08× быстрее) | 913 ns (в 1.94× медленнее) | 1.12 µs (в 2.42× быстрее) | 5.7 µs (в 1.94× быстрее) | 400.0 MiB | +| Doublets Split NonVolatile Uncached | 41.6 µs (в 7.02× медленнее) | 11.1 µs (в 23.7× медленнее) | 11.7 µs (в 4.3× медленнее) | 5.88 µs (в 1.88× быстрее) | 400.0 MiB | + +![Дуплеты на Rust против SQLite, 32 бит, объекты](docs/benchmarks/objects-rust-32.png) + +#### Тесты с 64-битным пространством адресов/идентификаторов + +##### 100,000 записей блога + +_5 повторов после прогрева, медианное время одной операции. SQLite 3.53.2, Ubuntu 24.04.5 LTS, AMD EPYC 7763 64-Core Processor, 4 cores, 16 GiB, [запуск GitHub Actions](https://github.com/linksplatform/Comparisons.SQLiteVSDoublets/actions/runs/37226110636) от 2026-10-04._ + +| Хранилище | Создание | Чтение всех | Чтение по id | Удаление | Размер файлов | +| --- | ---: | ---: | ---: | ---: | ---: | +| SQLite Memory | 1.22 µs | 548 ns | 1.44 µs | 1.7 µs | — | +| SQLite File | 2.01 µs | 625 ns | 2.6 µs | 7.27 µs | 78.3 MiB | +| Doublets United Volatile Cached | 5.17 µs (в 4.22× медленнее) | 1.46 µs (в 2.67× медленнее) | 2.25 µs (в 1.57× медленнее) | 6.12 µs (в 3.6× медленнее) | — | +| Doublets United Volatile Uncached | 95.7 µs (в 78.2× медленнее) | 9.79 µs (в 17.9× медленнее) | 10.7 µs (в 7.47× медленнее) | 5.33 µs (в 3.13× медленнее) | — | +| Doublets United NonVolatile Cached | 5.58 µs (в 2.78× медленнее) | 1.45 µs (в 2.32× медленнее) | 2.13 µs (в 1.22× быстрее) | 5.65 µs (в 1.29× быстрее) | 64.0 MiB | +| Doublets United NonVolatile Uncached | 99.7 µs (в 49.7× медленнее) | 9.73 µs (в 15.6× медленнее) | 11 µs (в 4.22× медленнее) | 5.68 µs (в 1.28× быстрее) | 64.0 MiB | +| Doublets Split Volatile Cached | 3.39 µs (в 2.77× медленнее) | 771 ns (в 1.41× медленнее) | 608 ns (в 2.37× быстрее) | 2.94 µs (в 1.73× медленнее) | — | +| Doublets Split Volatile Uncached | 48.1 µs (в 39.3× медленнее) | 14.1 µs (в 25.7× медленнее) | 14.5 µs (в 10.1× медленнее) | 2.97 µs (в 1.75× медленнее) | — | +| Doublets Split NonVolatile Cached | 3.75 µs (в 1.87× медленнее) | 801 ns (в 1.28× медленнее) | 634 ns (в 4.09× быстрее) | 3.68 µs (в 1.98× быстрее) | 80.0 MiB | +| Doublets Split NonVolatile Uncached | 47.4 µs (в 23.6× медленнее) | 14.1 µs (в 22.5× медленнее) | 14.6 µs (в 5.6× медленнее) | 3.23 µs (в 2.25× быстрее) | 80.0 MiB | + +##### 1,000,000 записей блога + +_1 повтор после прогрева, медианное время одной операции. SQLite 3.53.2, Ubuntu 24.04.5 LTS, INTEL(R) XEON(R) PLATINUM 8573C, 4 cores, 16 GiB, [запуск GitHub Actions](https://github.com/linksplatform/Comparisons.SQLiteVSDoublets/actions/runs/37226110636) от 2026-10-04._ + +| Хранилище | Создание | Чтение всех | Чтение по id | Удаление | Размер файлов | +| --- | ---: | ---: | ---: | ---: | ---: | +| SQLite Memory | 971 ns | 481 ns | 2.07 µs | 2.66 µs | — | +| SQLite File | 5.28 µs | 466 ns | 2.69 µs | 10.8 µs | 783.2 MiB | +| Doublets United Volatile Cached | 7.42 µs (в 7.64× медленнее) | 1.77 µs (в 3.69× медленнее) | 3.27 µs (в 1.58× медленнее) | 11 µs (в 4.13× медленнее) | — | +| Doublets United Volatile Uncached | 114 µs (в 117× медленнее) | 8.55 µs (в 17.8× медленнее) | 10.8 µs (в 5.19× медленнее) | 11.8 µs (в 4.44× медленнее) | — | +| Doublets United NonVolatile Cached | 9.17 µs (в 1.74× медленнее) | 1.72 µs (в 3.69× медленнее) | 3.45 µs (в 1.28× медленнее) | 11.8 µs (в 1.1× медленнее) | 640.0 MiB | +| Doublets United NonVolatile Uncached | 124 µs (в 23.4× медленнее) | 9.19 µs (в 19.7× медленнее) | 11.2 µs (в 4.17× медленнее) | 12.3 µs (в 1.14× медленнее) | 640.0 MiB | +| Doublets Split Volatile Cached | 4.83 µs (в 4.98× медленнее) | 919 ns (в 1.91× медленнее) | 1.19 µs (в 1.74× быстрее) | 6.82 µs (в 2.56× медленнее) | — | +| Doublets Split Volatile Uncached | 43.2 µs (в 44.5× медленнее) | 11 µs (в 22.9× медленнее) | 11.7 µs (в 5.65× медленнее) | 5.93 µs (в 2.23× медленнее) | — | +| Doublets Split NonVolatile Cached | 6.98 µs (в 1.32× медленнее) | 896 ns (в 1.92× медленнее) | 1.23 µs (в 2.18× быстрее) | 7.21 µs (в 1.49× быстрее) | 800.0 MiB | +| Doublets Split NonVolatile Uncached | 46.4 µs (в 8.78× медленнее) | 11.3 µs (в 24.3× медленнее) | 12 µs (в 4.45× медленнее) | 7.58 µs (в 1.42× быстрее) | 800.0 MiB | + +![Дуплеты на Rust против SQLite, 64 бит, объекты](docs/benchmarks/objects-rust-64.png) + +### Дуплеты на C# против SQLite + +#### Тесты с 32-битным пространством адресов/идентификаторов + +##### 100,000 записей блога + +_5 повторов после прогрева, медианное время одной операции. SQLite 3.53.3, Ubuntu 24.04.5 LTS, AMD EPYC 7763 64-Core Processor, 4 cores, 16 GiB, [запуск GitHub Actions](https://github.com/linksplatform/Comparisons.SQLiteVSDoublets/actions/runs/37226110636) от 2026-10-04._ + +| Хранилище | Создание | Чтение всех | Чтение по id | Удаление | Размер файлов | +| --- | ---: | ---: | ---: | ---: | ---: | +| SQLite Memory | 5.16 µs | 1.51 µs | 2.86 µs | 2.09 µs | — | +| SQLite File | 6.4 µs | 1.6 µs | 4.03 µs | 8.14 µs | 78.3 MiB | +| Doublets United Volatile Cached | 16.7 µs (в 3.23× медленнее) | 8.6 µs (в 5.68× медленнее) | 2.77 µs (≈ так же) | 11.7 µs (в 5.59× медленнее) | — | +| Doublets United Volatile Uncached | 178 µs (в 34.5× медленнее) | 322 µs (в 213× медленнее) | 324 µs (в 113× медленнее) | 10.7 µs (в 5.13× медленнее) | — | +| Doublets United NonVolatile Cached | 17 µs (в 2.65× медленнее) | 8.53 µs (в 5.31× медленнее) | 2.88 µs (в 1.4× быстрее) | 11.6 µs (в 1.43× медленнее) | 32.0 MiB | +| Doublets United NonVolatile Uncached | 178 µs (в 27.8× медленнее) | 330 µs (в 205× медленнее) | 331 µs (в 82.2× медленнее) | 12.4 µs (в 1.52× медленнее) | 32.0 MiB | +| Doublets Split Volatile Cached | 8.93 µs (в 1.73× медленнее) | 9.8 µs (в 6.48× медленнее) | 4.17 µs (в 1.46× медленнее) | 7.87 µs (в 3.77× медленнее) | — | +| Doublets Split Volatile Uncached | 113 µs (в 21.8× медленнее) | 389 µs (в 257× медленнее) | 392 µs (в 137× медленнее) | 7.86 µs (в 3.76× медленнее) | — | +| Doublets Split NonVolatile Cached | 9.64 µs (в 1.51× медленнее) | 9.74 µs (в 6.07× медленнее) | 4.26 µs (в 1.06× медленнее) | 8.29 µs (≈ так же) | 40.0 MiB | +| Doublets Split NonVolatile Uncached | 110 µs (в 17.2× медленнее) | 379 µs (в 236× медленнее) | 381 µs (в 94.6× медленнее) | 8.03 µs (≈ так же) | 40.0 MiB | + +##### 1,000,000 записей блога + +_1 повтор после прогрева, медианное время одной операции. SQLite 3.53.3, Ubuntu 24.04.5 LTS, AMD EPYC 9V74 80-Core Processor, 4 cores, 16 GiB, [запуск GitHub Actions](https://github.com/linksplatform/Comparisons.SQLiteVSDoublets/actions/runs/37226110636) от 2026-10-04._ + +| Хранилище | Создание | Чтение всех | Чтение по id | Удаление | Размер файлов | +| --- | ---: | ---: | ---: | ---: | ---: | +| SQLite Memory | 4.17 µs | 1.24 µs | 3.03 µs | 2.93 µs | — | +| SQLite File | 8.39 µs | 1.27 µs | 4.81 µs | 16 µs | 783.2 MiB | +| Doublets United Volatile Cached | 16.5 µs (в 3.96× медленнее) | 7.17 µs (в 5.77× медленнее) | 3.77 µs (в 1.25× медленнее) | 14 µs (в 4.77× медленнее) | — | +| Doublets United Volatile Uncached | 192 µs (в 46× медленнее) | 248 µs (в 200× медленнее) | 251 µs (в 82.9× медленнее) | 14.1 µs (в 4.8× медленнее) | — | +| Doublets United NonVolatile Cached | 23.3 µs (в 2.77× медленнее) | 7.07 µs (в 5.59× медленнее) | 4.05 µs (в 1.19× быстрее) | 14.3 µs (в 1.12× быстрее) | 320.0 MiB | +| Doublets United NonVolatile Uncached | 196 µs (в 23.3× медленнее) | 250 µs (в 197× медленнее) | 251 µs (в 52.2× медленнее) | 14.7 µs (в 1.09× быстрее) | 320.0 MiB | +| Doublets Split Volatile Cached | 7.97 µs (в 1.91× медленнее) | 8.1 µs (в 6.52× медленнее) | 3.82 µs (в 1.26× медленнее) | 9.2 µs (в 3.14× медленнее) | — | +| Doublets Split Volatile Uncached | 87.2 µs (в 20.9× медленнее) | 335 µs (в 270× медленнее) | 327 µs (в 108× медленнее) | 9.22 µs (в 3.15× медленнее) | — | +| Doublets Split NonVolatile Cached | 15.6 µs (в 1.86× медленнее) | 8.73 µs (в 6.9× медленнее) | 3.9 µs (в 1.23× быстрее) | 12.9 µs (в 1.25× быстрее) | 400.0 MiB | +| Doublets Split NonVolatile Uncached | 98.9 µs (в 11.8× медленнее) | 320 µs (в 253× медленнее) | 323 µs (в 67.2× медленнее) | 10.2 µs (в 1.57× быстрее) | 400.0 MiB | + +![Дуплеты на C# против SQLite, 32 бит, объекты](docs/benchmarks/objects-csharp-32.png) + +#### Тесты с 64-битным пространством адресов/идентификаторов + +##### 100,000 записей блога + +_5 повторов после прогрева, медианное время одной операции. SQLite 3.53.3, Ubuntu 24.04.5 LTS, AMD EPYC 7763 64-Core Processor, 4 cores, 16 GiB, [запуск GitHub Actions](https://github.com/linksplatform/Comparisons.SQLiteVSDoublets/actions/runs/37226110636) от 2026-10-04._ + +| Хранилище | Создание | Чтение всех | Чтение по id | Удаление | Размер файлов | +| --- | ---: | ---: | ---: | ---: | ---: | +| SQLite Memory | 5.06 µs | 1.43 µs | 2.73 µs | 1.9 µs | — | +| SQLite File | 6.09 µs | 1.56 µs | 3.81 µs | 7.65 µs | 78.3 MiB | +| Doublets United Volatile Cached | 17.8 µs (в 3.52× медленнее) | 9.46 µs (в 6.62× медленнее) | 2.83 µs (≈ так же) | 12.7 µs (в 6.7× медленнее) | — | +| Doublets United Volatile Uncached | 187 µs (в 36.9× медленнее) | 346 µs (в 243× медленнее) | 362 µs (в 133× медленнее) | 12.6 µs (в 6.65× медленнее) | — | +| Doublets United NonVolatile Cached | 18.5 µs (в 3.04× медленнее) | 9.61 µs (в 6.16× медленнее) | 3.09 µs (в 1.24× быстрее) | 12.9 µs (в 1.69× медленнее) | 64.0 MiB | +| Doublets United NonVolatile Uncached | 183 µs (в 30.1× медленнее) | 351 µs (в 225× медленнее) | 356 µs (в 93.3× медленнее) | 13.2 µs (в 1.72× медленнее) | 64.0 MiB | +| Doublets Split Volatile Cached | 8.24 µs (в 1.63× медленнее) | 9.57 µs (в 6.71× медленнее) | 4.19 µs (в 1.53× медленнее) | 7.52 µs (в 3.96× медленнее) | — | +| Doublets Split Volatile Uncached | 109 µs (в 21.5× медленнее) | 383 µs (в 268× медленнее) | 389 µs (в 142× медленнее) | 7.54 µs (в 3.97× медленнее) | — | +| Doublets Split NonVolatile Cached | 9.47 µs (в 1.56× медленнее) | 9.56 µs (в 6.13× медленнее) | 4.11 µs (в 1.08× медленнее) | 7.7 µs (≈ так же) | 80.0 MiB | +| Doublets Split NonVolatile Uncached | 108 µs (в 17.8× медленнее) | 377 µs (в 242× медленнее) | 385 µs (в 101× медленнее) | 8.35 µs (в 1.09× медленнее) | 80.0 MiB | + +##### 1,000,000 записей блога + +_1 повтор после прогрева, медианное время одной операции. SQLite 3.53.3, Ubuntu 24.04.5 LTS, AMD EPYC 7763 64-Core Processor, 4 cores, 16 GiB, [запуск GitHub Actions](https://github.com/linksplatform/Comparisons.SQLiteVSDoublets/actions/runs/37226110636) от 2026-10-04._ + +| Хранилище | Создание | Чтение всех | Чтение по id | Удаление | Размер файлов | +| --- | ---: | ---: | ---: | ---: | ---: | +| SQLite Memory | 5.71 µs | 1.59 µs | 3.41 µs | 3.18 µs | — | +| SQLite File | 7.73 µs | 1.66 µs | 5.17 µs | 11.5 µs | 783.2 MiB | +| Doublets United Volatile Cached | 22.3 µs (в 3.9× медленнее) | 9.91 µs (в 6.22× медленнее) | 4.25 µs (в 1.25× медленнее) | 20.3 µs (в 6.38× медленнее) | — | +| Doublets United Volatile Uncached | 232 µs (в 40.7× медленнее) | 318 µs (в 200× медленнее) | 320 µs (в 93.9× медленнее) | 18.2 µs (в 5.73× медленнее) | — | +| Doublets United NonVolatile Cached | 29.2 µs (в 3.78× медленнее) | 9.25 µs (в 5.58× медленнее) | 4.04 µs (в 1.28× быстрее) | 18.3 µs (в 1.59× медленнее) | 640.0 MiB | +| Doublets United NonVolatile Uncached | 236 µs (в 30.5× медленнее) | 315 µs (в 190× медленнее) | 319 µs (в 61.8× медленнее) | 20.7 µs (в 1.8× медленнее) | 640.0 MiB | +| Doublets Split Volatile Cached | 10.6 µs (в 1.85× медленнее) | 10.3 µs (в 6.46× медленнее) | 4.65 µs (в 1.36× медленнее) | 12 µs (в 3.76× медленнее) | — | +| Doublets Split Volatile Uncached | 111 µs (в 19.4× медленнее) | 387 µs (в 243× медленнее) | 405 µs (в 119× медленнее) | 13 µs (в 4.09× медленнее) | — | +| Doublets Split NonVolatile Cached | 20.4 µs (в 2.64× медленнее) | 11 µs (в 6.61× медленнее) | 5.27 µs (≈ так же) | 15 µs (в 1.3× медленнее) | 800.0 MiB | +| Doublets Split NonVolatile Uncached | 132 µs (в 17.1× медленнее) | 402 µs (в 242× медленнее) | 407 µs (в 78.7× медленнее) | 16.6 µs (в 1.45× медленнее) | 800.0 MiB | + +![Дуплеты на C# против SQLite, 64 бит, объекты](docs/benchmarks/objects-csharp-64.png) + + + +## Исходное сравнение + +Исходное сравнение объектов на C# и его исторические результаты. + + + +### SQLite -## SQLite ```C# using System.Linq; using Comparisons.SQLiteVSDoublets.Model; @@ -129,7 +503,8 @@ namespace Comparisons.SQLiteVSDoublets.SQLite } ``` -## Дуплеты +### Дуплеты + ``` C# using System.IO; using Platform.IO; @@ -187,18 +562,22 @@ namespace Comparisons.SQLiteVSDoublets.Doublets } ``` -## [Результат](https://www.icloud.com/keynote/0cYVNWkWD5RLU0k-XIBs3qWkA#Sqlite_vs_Doublets) +### [Результат](https://www.icloud.com/keynote/0cYVNWkWD5RLU0k-XIBs3qWkA#Sqlite_vs_Doublets) + +#### Производительность -### Производительность ![Изображение с результатом сравнения производительности SQLite и Дуплетов.](https://raw.githubusercontent.com/linksplatform/Documentation/master/doc/Examples/sqlite_vs_doublets_performance.png "Результат сравнения производительности SQLite и Дуплетов") -### Использование пространства на диске +#### Использование пространства на диске + ![Изображение с результатом сравнения использования пространства на диске SQLite и Дуплетов.](https://raw.githubusercontent.com/linksplatform/Documentation/master/doc/Examples/sqlite_vs_doublets_disk_usage.png "Результат сравнения использования пространства на диске SQLite и Дуплетов") -### Использование оперативной памяти +#### Использование оперативной памяти + ![Изображение с результатом сравнения использования оперативной памяти SQLite и Дуплетов.](https://raw.githubusercontent.com/linksplatform/Documentation/master/doc/Examples/sqlite_vs_doublets_ram_usage.png "Результат сравнения использования оперативной памяти SQLite и Дуплетов") -### Исходные данные +#### Исходные данные + ``` ini BenchmarkDotNet=v0.12.0, OS=Windows 10.0.18362 @@ -211,6 +590,7 @@ InvocationCount=1 IterationCount=1 UnrollFactor=1 WarmupCount=2 ``` + | Method | N | Mean | Error | Gen 0 | Gen 1 | Gen 2 | Allocated | SizeAfterCreation | |--------- |------- |------------:|------:|-------------:|------------:|------:|------------:|------------------:| | **SQLite** | **1000** | **719.1 ms** | **NA** | **5000.0000** | **-** | **-** | **30.67 MB** | **925696** | @@ -220,7 +600,10 @@ WarmupCount=2 | **SQLite** | **100000** | **35,853.8 ms** | **NA** | **680000.0000** | **151000.0000** | **-** | **3234.09 MB** | **90890240** | | Doublets | 100000 | 13,083.4 ms | NA | 3088000.0000 | 328000.0000 | - | 12356.33 MB | 64192256 | + -## Заключение +### Заключение -В этом конкретном сравнении Дуплеты работают быстрее и используют меньше памяти на диске, но это достигается за счёт дополнительного использования оперативной памяти (Sqlite использует её меньше). +В этом конкретном сравнении Дуплеты работают быстрее и используют меньше памяти +на диске, но это достигается за счёт дополнительного использования оперативной +памяти (Sqlite использует её меньше). diff --git a/csharp/Benchmarks.cs b/csharp/Benchmarks.cs deleted file mode 100644 index ee78467..0000000 --- a/csharp/Benchmarks.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System.IO; -using BenchmarkDotNet.Attributes; -using BenchmarkDotNet.Configs; -using Comparisons.SQLiteVSDoublets.Model; -using Comparisons.SQLiteVSDoublets.SQLite; -using Comparisons.SQLiteVSDoublets.Doublets; - -namespace Comparisons.SQLiteVSDoublets -{ - [SimpleJob] - [MemoryDiagnoser] - [WarmupCount(2)] - [IterationCount(1)] - [Config(typeof(Config))] - public class Benchmarks - { - private class Config : ManualConfig - { - public Config() => AddColumn(new SizeAfterCreationColumn()); - } - - [Params(1000, 10000, 100000)] - public int N; - private SQLiteTestRun _sqliteTestRun = null!; - private DoubletsTestRun _doubletsTestRun = null!; - - [GlobalSetup] - public void Setup() - { - BlogPosts.GenerateData(N); - _sqliteTestRun = new SQLiteTestRun("test.db"); - _doubletsTestRun = new DoubletsTestRun("test.links"); - } - - [Benchmark] - public void SQLite() => _sqliteTestRun.Run(); - - [IterationCleanup(Target = "SQLite")] - public void SQLiteOutput() - { - Directory.CreateDirectory(SizeAfterCreationColumn.DbSizeOutputFolder); - File.WriteAllText(Path.Combine(SizeAfterCreationColumn.DbSizeOutputFolder, $"disk-size.sqlite.{N}.txt"), _sqliteTestRun.Results.DbSizeAfterCreation.ToString()); - } - - [Benchmark] - public void Doublets() => _doubletsTestRun.Run(); - - [IterationCleanup(Target = "Doublets")] - public void DoubletsOutput() - { - Directory.CreateDirectory(SizeAfterCreationColumn.DbSizeOutputFolder); - File.WriteAllText(Path.Combine(SizeAfterCreationColumn.DbSizeOutputFolder, $"disk-size.doublets.{N}.txt"), _doubletsTestRun.Results.DbSizeAfterCreation.ToString()); - } - } -} diff --git a/csharp/Comparisons.SQLiteVSDoublets.csproj b/csharp/Comparisons.SQLiteVSDoublets.csproj deleted file mode 100644 index 58d6b82..0000000 --- a/csharp/Comparisons.SQLiteVSDoublets.csproj +++ /dev/null @@ -1,34 +0,0 @@ - - - - Exe - net8.0 - enable - - $(NoWarn);NETSDK1206 - - - - - - - - - - - - - - - - - - - - - - - diff --git a/csharp/Comparisons.SQLiteVSDoublets.sln b/csharp/Comparisons.SQLiteVSDoublets.sln deleted file mode 100644 index 98c6043..0000000 --- a/csharp/Comparisons.SQLiteVSDoublets.sln +++ /dev/null @@ -1,25 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 - VisualStudioVersion = 16.0.29123.88 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Comparisons.SQLiteVSDoublets", "Comparisons.SQLiteVSDoublets.csproj", "{2B2D5048-3A81-4ED5-91CD-32744C970875}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {2B2D5048-3A81-4ED5-91CD-32744C970875}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2B2D5048-3A81-4ED5-91CD-32744C970875}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2B2D5048-3A81-4ED5-91CD-32744C970875}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2B2D5048-3A81-4ED5-91CD-32744C970875}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {A6D1A7DB-8A3E-4789-8F89-8A343C96CCBD} - EndGlobalSection -EndGlobal diff --git a/csharp/Doublets/DoubletsDbContext.cs b/csharp/Doublets/DoubletsDbContext.cs deleted file mode 100644 index b1b7ecd..0000000 --- a/csharp/Doublets/DoubletsDbContext.cs +++ /dev/null @@ -1,414 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using Platform.Disposables; -using Platform.Collections.Lists; -using Platform.Collections.Stacks; -using Platform.Converters; -using Platform.Memory; -using Platform.Data; -using Platform.Data.Numbers.Raw; -using Platform.Data.Doublets; -using Platform.Data.Doublets.Decorators; -using Platform.Data.Doublets.PropertyOperators; -using Platform.Data.Doublets.CriterionMatchers; -using Platform.Data.Doublets.Sequences; -using Platform.Data.Doublets.Sequences.Unicode; -using Platform.Data.Doublets.Sequences.Time; -using Platform.Data.Doublets.Sequences.Walkers; -using Platform.Data.Doublets.Sequences.Converters; -using Platform.Data.Doublets.Sequences.CriterionMatchers; -using Platform.Data.Doublets.Sequences.Numbers.Raw; -using Platform.Data.Doublets.Memory.Split.Generic; -using Comparisons.SQLiteVSDoublets.Model; -using TLinkAddress = System.UInt32; - -namespace Comparisons.SQLiteVSDoublets.Doublets -{ - /// - /// - /// Represents the doublets db context. - /// - /// - /// - /// - public class DoubletsDbContext : DisposableBase - { - /// - /// - /// The meaning root. - /// - /// - /// - private readonly TLinkAddress _meaningRoot; - /// - /// - /// The unicode symbol marker. - /// - /// - /// - private readonly TLinkAddress _unicodeSymbolMarker; - /// - /// - /// The unicode sequence marker. - /// - /// - /// - private readonly TLinkAddress _unicodeSequenceMarker; - /// - /// - /// The title property marker. - /// - /// - /// - private readonly TLinkAddress _titlePropertyMarker; - /// - /// - /// The content property marker. - /// - /// - /// - private readonly TLinkAddress _contentPropertyMarker; - /// - /// - /// The publication date time property marker. - /// - /// - /// - private readonly TLinkAddress _publicationDateTimePropertyMarker; - /// - /// - /// The blog post marker. - /// - /// - /// - private readonly TLinkAddress _blogPostMarker; - /// - /// - /// The default link property operator. - /// - /// - /// - private readonly PropertiesOperator _defaultLinkPropertyOperator; - /// - /// - /// The number to address converter. - /// - /// - /// - private readonly RawNumberToAddressConverter _numberToAddressConverter; - /// - /// - /// The address to number converter. - /// - /// - /// - private readonly AddressToRawNumberConverter _addressToNumberConverter; - /// - /// - /// The long raw number to date time converter. - /// - /// - /// - private readonly LongRawNumberSequenceToDateTimeConverter _longRawNumberToDateTimeConverter; - /// - /// - /// The date time to long raw number converter. - /// - /// - /// - private readonly DateTimeToLongRawNumberSequenceConverter _dateTimeToLongRawNumberConverter; - /// - /// - /// The string to unicode sequence converter. - /// - /// - /// - private readonly IConverter _stringToUnicodeSequenceConverter; - /// - /// - /// The unicode sequence to string converter. - /// - /// - /// - private readonly IConverter _unicodeSequenceToStringConverter; - /// - /// - /// The disposable links. - /// - /// - /// - private readonly ILinks _disposableLinks; - /// - /// - /// The links. - /// - /// - /// - private readonly ILinks _links; - - /// - /// - /// Initializes a new instance. - /// - /// - /// - /// - /// A data db filename. - /// - /// - /// - /// A index db filename. - /// - /// - public DoubletsDbContext(string dataDBFilename, string indexDBFilename) - { - var dataMemory = new FileMappedResizableDirectMemory(dataDBFilename); - var indexMemory = new FileMappedResizableDirectMemory(indexDBFilename); - - var linksConstants = new LinksConstants(enableExternalReferencesSupport: true); - - // Init the links storage - _disposableLinks = new SplitMemoryLinks(dataMemory, indexMemory); - _links = _disposableLinks; - - // Set up constant links (markers, aka mapped links) - TLinkAddress currentMappingLinkIndex = 1; - _meaningRoot = GetOrCreateMeaningRoot(currentMappingLinkIndex++); - _unicodeSymbolMarker = GetOrCreateNextMapping(currentMappingLinkIndex++); - _unicodeSequenceMarker = GetOrCreateNextMapping(currentMappingLinkIndex++); - _titlePropertyMarker = GetOrCreateNextMapping(currentMappingLinkIndex++); - _contentPropertyMarker = GetOrCreateNextMapping(currentMappingLinkIndex++); - _publicationDateTimePropertyMarker = GetOrCreateNextMapping(currentMappingLinkIndex++); - _blogPostMarker = GetOrCreateNextMapping(currentMappingLinkIndex++); - - // Create properties operator that is able to control reading and writing properties for any link (object) - _defaultLinkPropertyOperator = new PropertiesOperator(_links); - - // Create converters that are able to convert link's address (UInt64 value) to a raw number represented with another UInt64 value and back - _numberToAddressConverter = new RawNumberToAddressConverter(); - _addressToNumberConverter = new AddressToRawNumberConverter(); - - // Create converters for dates - _longRawNumberToDateTimeConverter = new LongRawNumberSequenceToDateTimeConverter(new LongRawNumberSequenceToNumberConverter(_links, _numberToAddressConverter)); - _dateTimeToLongRawNumberConverter = new DateTimeToLongRawNumberSequenceConverter(new NumberToLongRawNumberSequenceConverter(_links, _addressToNumberConverter)); - - // Create converters that are able to convert string to unicode sequence stored as link and back - var balancedVariantConverter = new BalancedVariantConverter(_links); - var unicodeSymbolCriterionMatcher = new TargetMatcher(_links, _unicodeSymbolMarker); - var unicodeSequenceCriterionMatcher = new TargetMatcher(_links, _unicodeSequenceMarker); - var charToUnicodeSymbolConverter = new CharToUnicodeSymbolConverter(_links, _addressToNumberConverter, _unicodeSymbolMarker); - var unicodeSymbolToCharConverter = new UnicodeSymbolToCharConverter(_links, _numberToAddressConverter, unicodeSymbolCriterionMatcher); - var sequenceWalker = new RightSequenceWalker(_links, new DefaultStack(), unicodeSymbolCriterionMatcher.IsMatched); - _stringToUnicodeSequenceConverter = new CachingConverterDecorator(new StringToUnicodeSequenceConverter(_links, charToUnicodeSymbolConverter, balancedVariantConverter, _unicodeSequenceMarker)); - _unicodeSequenceToStringConverter = new CachingConverterDecorator(new UnicodeSequenceToStringConverter(_links, unicodeSequenceCriterionMatcher, sequenceWalker, unicodeSymbolToCharConverter, _unicodeSequenceMarker)); - } - - /// - /// - /// Gets the or create meaning root using the specified meaning root index. - /// - /// - /// - /// - /// The meaning root index. - /// - /// - /// - /// The link address - /// - /// - private TLinkAddress GetOrCreateMeaningRoot(TLinkAddress meaningRootIndex) => _links.Exists(meaningRootIndex) ? meaningRootIndex : _links.CreatePoint(); - - /// - /// - /// Gets the or create next mapping using the specified current mapping index. - /// - /// - /// - /// - /// The current mapping index. - /// - /// - /// - /// The link address - /// - /// - private TLinkAddress GetOrCreateNextMapping(TLinkAddress currentMappingIndex) => _links.Exists(currentMappingIndex) ? currentMappingIndex : _links.CreateAndUpdate(_meaningRoot, _links.Constants.Itself); - - /// - /// - /// Converts the to string using the specified sequence. - /// - /// - /// - /// - /// The sequence. - /// - /// - /// - /// The string - /// - /// - public string ConvertToString(TLinkAddress sequence) => _unicodeSequenceToStringConverter.Convert(sequence); - - /// - /// - /// Converts the to sequence using the specified string. - /// - /// - /// - /// - /// The string. - /// - /// - /// - /// The link address - /// - /// - public TLinkAddress ConvertToSequence(string @string) => _stringToUnicodeSequenceConverter.Convert(@string); - - /// - /// - /// Gets the blog posts value. - /// - /// - /// - public IList BlogPosts => GetBlogPosts(); - - /// - /// - /// Gets the blog posts. - /// - /// - /// - /// - /// A list of blog post - /// - /// - public IList GetBlogPosts() - { - var list = new List>(); - var listFiller = new ListFiller, TLinkAddress>(list, _links.Constants.Continue); - // Load all links that match the query: (any: _blogPostMarker any) it means, link with any address, _blogPostMarker as source and any link as target. - // All links that match this query are BlogPosts. - var any = _links.Constants.Any; - var query = new Link(any, _blogPostMarker, any); - _links.Each(element => listFiller.AddAndReturnConstant(element!), query); - return list.Select(LoadBlogPost).ToList(); - } - - /// - /// - /// Loads the blog post using the specified post link. - /// - /// - /// - /// - /// The post link. - /// - /// - /// - /// The blog post - /// - /// - public BlogPost LoadBlogPost(IList postLink) => LoadBlogPost(postLink[_links.Constants.IndexPart]); - - /// - /// - /// Loads the blog post using the specified post link. - /// - /// - /// - /// - /// The post link. - /// - /// - /// - /// The blog post. - /// - /// - public BlogPost LoadBlogPost(TLinkAddress postLink) - { - var blogPost = new BlogPost(); - - blogPost.Id = (int)postLink; - - // Load Title property value from the links storage - var titleSequence = _defaultLinkPropertyOperator.GetValue(postLink, _titlePropertyMarker); - blogPost.Title = ConvertToString(titleSequence); - - // Load Content property value from the links storage - var contentSequence = _defaultLinkPropertyOperator.GetValue(postLink, _contentPropertyMarker); - blogPost.Content = ConvertToString(contentSequence); - - // Load PublicationDateTime property value from the links storage - blogPost.PublicationDateTime = _longRawNumberToDateTimeConverter.Convert(_defaultLinkPropertyOperator.GetValue(postLink, _publicationDateTimePropertyMarker)); - - return blogPost; - } - - /// - /// - /// Saves the blog post using the specified post. - /// - /// - /// - /// - /// The post. - /// - /// - /// - /// The new post link. - /// - /// - public TLinkAddress SaveBlogPost(BlogPost post) - { - var newPostLink = _links.CreateAndUpdate(_blogPostMarker, _links.Constants.Itself); - - // Save Title property value to the links storage - _defaultLinkPropertyOperator.SetValue(newPostLink, _titlePropertyMarker, ConvertToSequence(post.Title)); - - // Save Content property value to the links storage - _defaultLinkPropertyOperator.SetValue(newPostLink, _contentPropertyMarker, ConvertToSequence(post.Content)); - - // Save PublicationDateTime property value to the links storage - _defaultLinkPropertyOperator.SetValue(newPostLink, _publicationDateTimePropertyMarker, _dateTimeToLongRawNumberConverter.Convert(post.PublicationDateTime)); - - return newPostLink; - } - - /// - /// - /// Deletes the link. - /// - /// - /// - /// - /// The link. - /// - /// - public void Delete(TLinkAddress link) => _links.Delete(link); - - /// - /// - /// Disposes the manual. - /// - /// - /// - /// - /// The manual. - /// - /// - /// - /// The was disposed. - /// - /// - protected override void Dispose(bool manual, bool wasDisposed) - { - if (!wasDisposed) - { - _disposableLinks.DisposeIfPossible(); - } - } - } -} diff --git a/csharp/Doublets/DoubletsTestRun.cs b/csharp/Doublets/DoubletsTestRun.cs deleted file mode 100644 index f00e11d..0000000 --- a/csharp/Doublets/DoubletsTestRun.cs +++ /dev/null @@ -1,117 +0,0 @@ -using System.IO; -using Platform.IO; -using Comparisons.SQLiteVSDoublets.Model; - -namespace Comparisons.SQLiteVSDoublets.Doublets -{ - /// - /// - /// Represents the doublets test run. - /// - /// - /// - /// - public class DoubletsTestRun : TestRun - { - /// - /// - /// Gets the db index filename value. - /// - /// - /// - public string DbIndexFilename { get; } - - /// - /// - /// Initializes a new instance. - /// - /// - /// - /// - /// A db filename. - /// - /// - public DoubletsTestRun(string dbFilename) : base(dbFilename) => DbIndexFilename = $"{Path.GetFileNameWithoutExtension(dbFilename)}.links.index"; - - /// - /// - /// Prepares this instance. - /// - /// - /// - public override void Prepare() - { - using var dbContext = new DoubletsDbContext(DbFilename, DbIndexFilename); - } - - /// - /// - /// Creates the list. - /// - /// - /// - public override void CreateList() - { - using var dbContext = new DoubletsDbContext(DbFilename, DbIndexFilename); - foreach (var blogPost in BlogPosts.List) - { - dbContext.SaveBlogPost(blogPost); - } - } - - /// - /// - /// Reads the list. - /// - /// - /// - public override void ReadList() - { - using var dbContext = new DoubletsDbContext(DbFilename, DbIndexFilename); - foreach (var blogPost in dbContext.BlogPosts) - { - ReadBlogPosts.Add(blogPost); - } - } - - /// - /// - /// Deletes the list. - /// - /// - /// - public override void DeleteList() - { - using var dbContext = new DoubletsDbContext(DbFilename, DbIndexFilename); - var blogPostsToDelete = dbContext.BlogPosts; - foreach (var blogPost in blogPostsToDelete) - { - dbContext.Delete((uint)blogPost.Id); - } - } - - /// - /// - /// Deletes the database. - /// - /// - /// - protected override void DeleteDatabase() - { - File.Delete(DbFilename); - File.Delete(DbIndexFilename); - } - - /// - /// - /// Gets the database size in bytes. - /// - /// - /// - /// - /// The long - /// - /// - protected override long GetDatabaseSizeInBytes() => FileHelpers.GetSize(DbFilename) + FileHelpers.GetSize(DbIndexFilename); - } -} diff --git a/csharp/Links/DoubletsLinksStorage.cs b/csharp/Links/DoubletsLinksStorage.cs deleted file mode 100644 index e899390..0000000 --- a/csharp/Links/DoubletsLinksStorage.cs +++ /dev/null @@ -1,78 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using Platform.Data.Doublets; -using Platform.Disposables; - -namespace Comparisons.SQLiteVSDoublets.Links; - -public sealed class DoubletsLinksStorage : ILinksStorage -{ - private readonly ILinks _links; - private readonly IReadOnlyList _paths; - - public DoubletsLinksStorage(ILinks links, params string[] paths) - { - _links = links; - _paths = paths; - } - - public uint Create(uint source, uint target) => _links.CreateAndUpdate(source, target); - - public uint CreatePoint() => _links.CreatePoint(); - - public void Update(uint id, uint source, uint target) => _links.Update(id, source, target); - - public void Delete(uint id) => Platform.Data.ILinksExtensions.Delete(_links, id); - - public IReadOnlyList QueryAll() - { - var any = _links.Constants.Any; - return Convert(_links.All(any, any, any)); - } - - public LinkRecord? QueryById(uint id) - { - var any = _links.Constants.Any; - var result = _links.All(id, any, any); - return result.Count == 0 || result[0] is null ? null : Convert(result[0]!); - } - - public IReadOnlyList QueryBySourceTarget(uint source, uint target) - { - var any = _links.Constants.Any; - return Convert(_links.All(any, source, target)); - } - - public IReadOnlyList QueryBySource(uint source) - { - var any = _links.Constants.Any; - return Convert(_links.All(any, source, any)); - } - - public IReadOnlyList QueryByTarget(uint target) - { - var any = _links.Constants.Any; - return Convert(_links.All(any, any, target)); - } - - public int Count => QueryAll().Count; - - public void Dispose() - { - _links.DisposeIfPossible(); - foreach (var path in _paths.Where(File.Exists)) - { - File.Delete(path); - } - } - - private LinkRecord Convert(IList link) => new( - _links.GetIndex(link), - _links.GetSource(link), - _links.GetTarget(link)); - - private List Convert(IList?> links) => - links.Where(link => link is not null).Select(link => Convert(link!)).ToList(); -} diff --git a/csharp/Links/ILinksStorage.cs b/csharp/Links/ILinksStorage.cs deleted file mode 100644 index 606f5a7..0000000 --- a/csharp/Links/ILinksStorage.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Comparisons.SQLiteVSDoublets.Links; - -public interface ILinksStorage : IDisposable -{ - uint Create(uint source, uint target); - - uint CreatePoint(); - - void Update(uint id, uint source, uint target); - - void Delete(uint id); - - IReadOnlyList QueryAll(); - - LinkRecord? QueryById(uint id); - - IReadOnlyList QueryBySourceTarget(uint source, uint target); - - IReadOnlyList QueryBySource(uint source); - - IReadOnlyList QueryByTarget(uint target); - - int Count { get; } -} diff --git a/csharp/Links/LinkRecord.cs b/csharp/Links/LinkRecord.cs deleted file mode 100644 index 3b75d83..0000000 --- a/csharp/Links/LinkRecord.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace Comparisons.SQLiteVSDoublets.Links; - -public readonly record struct LinkRecord(uint Id, uint Source, uint Target); diff --git a/csharp/Links/LinksData.cs b/csharp/Links/LinksData.cs deleted file mode 100644 index 4bf84d2..0000000 --- a/csharp/Links/LinksData.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Comparisons.SQLiteVSDoublets.Links; - -public static class LinksData -{ - public static uint[] FillBackground(ILinksStorage storage, int count) - { - var ids = new uint[count]; - for (var index = 0; index < count; index++) - { - ids[index] = storage.CreatePoint(); - } - return ids; - } - - public static LinkRecord[] FillBenchmarked( - ILinksStorage storage, - IReadOnlyList background, - int count) - { - if (background.Count == 0) - { - throw new ArgumentException("Background links are required.", nameof(background)); - } - - var links = new LinkRecord[count]; - for (var index = 0; index < count; index++) - { - var source = background[index % background.Count]; - var target = background[((index % background.Count) + 1 + (index / background.Count)) - % background.Count]; - links[index] = new LinkRecord(storage.Create(source, target), source, target); - } - return links; - } -} diff --git a/csharp/Links/LinksStorageFactory.cs b/csharp/Links/LinksStorageFactory.cs deleted file mode 100644 index d6bad6a..0000000 --- a/csharp/Links/LinksStorageFactory.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System; -using System.IO; -using Platform.Data.Doublets; -using Platform.Data.Doublets.Memory.Split.Generic; -using Platform.Data.Doublets.Memory.United.Generic; -using Platform.Memory; - -namespace Comparisons.SQLiteVSDoublets.Links; - -public enum LinksVariant -{ - SQLite_Memory, - SQLite_File, - Doublets_United_Volatile, - Doublets_United_NonVolatile, - Doublets_Split_Volatile, - Doublets_Split_NonVolatile, -} - -public static class LinksStorageFactory -{ - public static ILinksStorage Create(LinksVariant variant) => variant switch - { - LinksVariant.SQLite_Memory => new SQLiteLinksStorage(inMemory: true), - LinksVariant.SQLite_File => new SQLiteLinksStorage(inMemory: false), - LinksVariant.Doublets_United_Volatile => new DoubletsLinksStorage( - new UnitedMemoryLinks(new HeapResizableDirectMemory())), - LinksVariant.Doublets_United_NonVolatile => CreateUnitedFile(), - LinksVariant.Doublets_Split_Volatile => new DoubletsLinksStorage( - new SplitMemoryLinks( - new HeapResizableDirectMemory(), - new HeapResizableDirectMemory())), - LinksVariant.Doublets_Split_NonVolatile => CreateSplitFile(), - _ => throw new ArgumentOutOfRangeException(nameof(variant), variant, null), - }; - - private static DoubletsLinksStorage CreateUnitedFile() - { - var path = TemporaryPath("united.links"); - ILinks links = new UnitedMemoryLinks(path); - return new DoubletsLinksStorage(links, path); - } - - private static DoubletsLinksStorage CreateSplitFile() - { - var dataPath = TemporaryPath("split.data.links"); - var indexPath = TemporaryPath("split.index.links"); - ILinks links = new SplitMemoryLinks(dataPath, indexPath); - return new DoubletsLinksStorage(links, dataPath, indexPath); - } - - private static string TemporaryPath(string suffix) => Path.Combine( - Path.GetTempPath(), - $"sqlite-vs-doublets-{Guid.NewGuid():N}-{suffix}"); -} diff --git a/csharp/Links/SQLiteLinksStorage.cs b/csharp/Links/SQLiteLinksStorage.cs deleted file mode 100644 index f46a53b..0000000 --- a/csharp/Links/SQLiteLinksStorage.cs +++ /dev/null @@ -1,150 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using Microsoft.Data.Sqlite; - -namespace Comparisons.SQLiteVSDoublets.Links; - -public sealed class SQLiteLinksStorage : ILinksStorage -{ - private readonly SqliteConnection _connection; - private readonly string? _path; - private uint _nextId = 1; - - public SQLiteLinksStorage(bool inMemory) - { - _path = inMemory - ? null - : Path.Combine(Path.GetTempPath(), $"sqlite-vs-doublets-{Guid.NewGuid():N}.db"); - var dataSource = _path ?? ":memory:"; - var connectionString = new SqliteConnectionStringBuilder - { - DataSource = dataSource, - // A pooled native connection keeps temporary database files open - // after Dispose, which prevents their immediate removal on Windows. - Pooling = false, - }.ToString(); - _connection = new SqliteConnection(connectionString); - _connection.Open(); - using var command = _connection.CreateCommand(); - command.CommandText = """ - CREATE TABLE links ( - id INTEGER PRIMARY KEY, - source INTEGER NOT NULL, - target INTEGER NOT NULL - ); - CREATE INDEX idx_source ON links(source); - CREATE INDEX idx_target ON links(target); - CREATE INDEX idx_source_target ON links(source, target); - """; - command.ExecuteNonQuery(); - } - - public uint Create(uint source, uint target) - { - var id = _nextId++; - using var command = Command( - "INSERT INTO links (id, source, target) VALUES ($id, $source, $target)", - ("$id", id), - ("$source", source), - ("$target", target)); - command.ExecuteNonQuery(); - return id; - } - - public uint CreatePoint() - { - var id = Create(0, 0); - Update(id, id, id); - return id; - } - - public void Update(uint id, uint source, uint target) - { - using var command = Command( - "UPDATE links SET source = $source, target = $target WHERE id = $id", - ("$source", source), - ("$target", target), - ("$id", id)); - command.ExecuteNonQuery(); - } - - public void Delete(uint id) - { - using var command = Command("DELETE FROM links WHERE id = $id", ("$id", id)); - command.ExecuteNonQuery(); - } - - public IReadOnlyList QueryAll() => Query( - "SELECT id, source, target FROM links"); - - public LinkRecord? QueryById(uint id) - { - var links = Query( - "SELECT id, source, target FROM links WHERE id = $id", - ("$id", id)); - return links.Count == 0 ? null : links[0]; - } - - public IReadOnlyList QueryBySourceTarget(uint source, uint target) => Query( - "SELECT id, source, target FROM links WHERE source = $source AND target = $target", - ("$source", source), - ("$target", target)); - - public IReadOnlyList QueryBySource(uint source) => Query( - "SELECT id, source, target FROM links WHERE source = $source", - ("$source", source)); - - public IReadOnlyList QueryByTarget(uint target) => Query( - "SELECT id, source, target FROM links WHERE target = $target", - ("$target", target)); - - public int Count - { - get - { - using var command = _connection.CreateCommand(); - command.CommandText = "SELECT COUNT(*) FROM links"; - return Convert.ToInt32(command.ExecuteScalar()); - } - } - - public void Dispose() - { - _connection.Dispose(); - if (_path is not null) - { - File.Delete(_path); - } - } - - private SqliteCommand Command( - string sql, - params (string Name, uint Value)[] parameters) - { - var command = _connection.CreateCommand(); - command.CommandText = sql; - foreach (var parameter in parameters) - { - command.Parameters.AddWithValue(parameter.Name, parameter.Value); - } - return command; - } - - private List Query( - string sql, - params (string Name, uint Value)[] parameters) - { - using var command = Command(sql, parameters); - using var reader = command.ExecuteReader(); - var result = new List(); - while (reader.Read()) - { - result.Add(new LinkRecord( - checked((uint)reader.GetInt64(0)), - checked((uint)reader.GetInt64(1)), - checked((uint)reader.GetInt64(2)))); - } - return result; - } -} diff --git a/csharp/LinksBenchmarks.cs b/csharp/LinksBenchmarks.cs deleted file mode 100644 index bf3545f..0000000 --- a/csharp/LinksBenchmarks.cs +++ /dev/null @@ -1,149 +0,0 @@ -using System; -using System.Collections.Generic; -using BenchmarkDotNet.Attributes; -using Comparisons.SQLiteVSDoublets.Links; - -namespace Comparisons.SQLiteVSDoublets; - -[MemoryDiagnoser] -public class LinksBenchmarks -{ - private ILinksStorage _storage = null!; - private uint[] _background = null!; - private uint[] _updateSources = null!; - private LinkRecord[] _links = null!; - - [ParamsAllValues] - public LinksVariant Variant { get; set; } - - [ParamsSource(nameof(LinkCounts))] - public int N { get; set; } - - public IEnumerable LinkCounts => new[] { EnvironmentValue("BENCHMARK_LINK_COUNT", 1000) }; - - [IterationSetup(Target = nameof(Create))] - public void SetupCreate() => Setup(createBenchmarked: false); - - [IterationSetup(Target = nameof(Update))] - public void SetupUpdate() - { - Setup(createBenchmarked: true); - _updateSources = LinksData.FillBackground(_storage, N); - } - - [IterationSetup(Target = nameof(Delete))] - public void SetupDelete() => Setup(createBenchmarked: true); - - [IterationSetup(Target = nameof(EachAll))] - public void SetupEachAll() => Setup(createBenchmarked: true); - - [IterationSetup(Target = nameof(EachIdentity))] - public void SetupEachIdentity() => Setup(createBenchmarked: true); - - [IterationSetup(Target = nameof(EachConcrete))] - public void SetupEachConcrete() => Setup(createBenchmarked: true); - - [IterationSetup(Target = nameof(EachOutgoing))] - public void SetupEachOutgoing() => Setup(createBenchmarked: true); - - [IterationSetup(Target = nameof(EachIncoming))] - public void SetupEachIncoming() => Setup(createBenchmarked: true); - - [IterationCleanup] - public void Cleanup() => _storage.Dispose(); - - [Benchmark] - public uint Create() - { - var created = LinksData.FillBenchmarked(_storage, _background, N); - return created[^1].Id; - } - - [Benchmark] - public uint Update() - { - var last = 0U; - for (var index = 0; index < _links.Length; index++) - { - _storage.Update(_links[index].Id, _updateSources[index], _background[0]); - last = _links[index].Id; - } - return last; - } - - [Benchmark] - public uint Delete() - { - var last = 0U; - foreach (var link in _links) - { - _storage.Delete(link.Id); - last = link.Id; - } - return last; - } - - [Benchmark] - public int EachAll() => _storage.QueryAll().Count; - - [Benchmark] - public ulong EachIdentity() - { - var checksum = 0UL; - foreach (var link in _links) - { - checksum += _storage.QueryById(link.Id)?.Id ?? 0; - } - return checksum; - } - - [Benchmark] - public int EachConcrete() - { - var count = 0; - foreach (var link in _links) - { - count += _storage.QueryBySourceTarget(link.Source, link.Target).Count; - } - return count; - } - - [Benchmark] - public int EachOutgoing() - { - var count = 0; - foreach (var link in _links) - { - count += _storage.QueryBySource(link.Source).Count; - } - return count; - } - - [Benchmark] - public int EachIncoming() - { - var count = 0; - foreach (var link in _links) - { - count += _storage.QueryByTarget(link.Target).Count; - } - return count; - } - - private void Setup(bool createBenchmarked) - { - _storage = LinksStorageFactory.Create(Variant); - _background = LinksData.FillBackground( - _storage, - EnvironmentValue("BACKGROUND_LINK_COUNT", 3000)); - _links = createBenchmarked - ? LinksData.FillBenchmarked(_storage, _background, N) - : Array.Empty(); - _updateSources = Array.Empty(); - } - - private static int EnvironmentValue(string name, int fallback) => - int.TryParse(Environment.GetEnvironmentVariable(name), out var value) && value > 0 - ? value - : fallback; -} diff --git a/csharp/LinksSelfTest.cs b/csharp/LinksSelfTest.cs deleted file mode 100644 index 4ef627d..0000000 --- a/csharp/LinksSelfTest.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System; -using System.Linq; -using Comparisons.SQLiteVSDoublets.Links; - -namespace Comparisons.SQLiteVSDoublets; - -public static class LinksSelfTest -{ - public static void Run() - { - foreach (var variant in Enum.GetValues()) - { - Check(variant); - Console.WriteLine($"{variant}: passed"); - } - } - - private static void Check(LinksVariant variant) - { - using var storage = LinksStorageFactory.Create(variant); - var background = LinksData.FillBackground(storage, 8); - var created = LinksData.FillBenchmarked(storage, background, 16); - Ensure(storage.Count == 24, $"{variant}: unexpected link count"); - Ensure(storage.QueryAll().Count == 24, $"{variant}: query all failed"); - - var sample = created[created.Length / 2]; - Ensure(storage.QueryById(sample.Id) == sample, $"{variant}: identity query failed"); - Ensure( - storage.QueryBySourceTarget(sample.Source, sample.Target).Contains(sample), - $"{variant}: concrete query failed"); - Ensure( - storage.QueryBySource(sample.Source).Contains(sample), - $"{variant}: outgoing query failed"); - Ensure( - storage.QueryByTarget(sample.Target).Contains(sample), - $"{variant}: incoming query failed"); - - var unused = storage.CreatePoint(); - storage.Update(sample.Id, unused, background[0]); - var updated = storage.QueryById(sample.Id); - Ensure( - updated is { Source: var source, Target: var target } - && source == unused - && target == background[0], - $"{variant}: update failed"); - - storage.Delete(sample.Id); - Ensure(storage.QueryById(sample.Id) is null, $"{variant}: delete failed"); - } - - private static void Ensure(bool condition, string message) - { - if (!condition) - { - throw new InvalidOperationException(message); - } - } -} diff --git a/csharp/Model/BlogPost.cs b/csharp/Model/BlogPost.cs deleted file mode 100644 index 16ad446..0000000 --- a/csharp/Model/BlogPost.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System; -using System.ComponentModel.DataAnnotations; - -namespace Comparisons.SQLiteVSDoublets.Model -{ - /// - /// - /// Represents the blog post. - /// - /// - /// - public class BlogPost - { - /// - /// - /// Gets or sets the id value. - /// - /// - /// - [Key] - public int Id { get; set; } - - /// - /// - /// Gets or sets the title value. - /// - /// - /// - [Required] - public string Title { get; set; } = string.Empty; - - /// - /// - /// Gets or sets the content value. - /// - /// - /// - [Required] - public string Content { get; set; } = string.Empty; - - /// - /// - /// Gets or sets the publication date time value. - /// - /// - /// - [Required] - public DateTime PublicationDateTime { get; set; } - - /// - /// - /// Returns the string. - /// - /// - /// - /// - /// The string - /// - /// - public override string ToString() => $"ID={Id}\tTitle={Title}\tContent=\tPublicationDateTime={PublicationDateTime}"; - } -} diff --git a/csharp/Model/BlogPosts.cs b/csharp/Model/BlogPosts.cs deleted file mode 100644 index 39d577e..0000000 --- a/csharp/Model/BlogPosts.cs +++ /dev/null @@ -1,68 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; - -namespace Comparisons.SQLiteVSDoublets.Model -{ - /// - /// - /// Represents the blog posts. - /// - /// - /// - public static class BlogPosts - { - /// - /// - /// Gets or sets the list value. - /// - /// - /// - public static IReadOnlyList List { get; private set; } - - /// - /// - /// Initializes a new instance. - /// - /// - /// - static BlogPosts() - { - List = new List(); - } - - /// - /// - /// Generates the data using the specified number of records. - /// - /// - /// - /// - /// The number of records. - /// - /// - static public void GenerateData(int numberOfRecords) - { - var contentStrings = new string[] - { - "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis malesuada blandit mauris nec bibendum. Phasellus feugiat vehicula mauris et aliquet. Integer et gravida velit, in rutrum leo. Duis pretium, nunc ac posuere porttitor, augue sapien commodo tortor, nec consequat lorem eros ultricies odio. Aliquam varius congue ex nec viverra. Pellentesque eu velit tellus. Donec ac luctus nisi. Curabitur dignissim sodales mauris eu semper. Ut pretium lorem nulla, sit amet auctor arcu placerat vitae. Quisque lacinia dolor et consectetur fermentum. Nam ac orci vitae nulla aliquam tempor ac a nibh. Ut ac tincidunt lacus. Morbi vitae felis lorem.", - "Curabitur tincidunt nibh sit amet finibus dictum. Suspendisse aliquet arcu non rutrum ultrices. Integer ullamcorper mauris sit amet nibh aliquam, et tempor turpis hendrerit. In molestie elit et mauris rutrum, non auctor ligula ultricies. Vestibulum dignissim mauris finibus libero interdum hendrerit. Nunc vitae ipsum porttitor, egestas magna ut, sagittis sem. Donec euismod ac tortor vel porta. Vivamus convallis, ex at vestibulum rutrum, velit purus venenatis metus, sit amet aliquam sapien nibh quis elit. Aenean id neque a orci sodales venenatis. Integer ut orci ligula. Interdum et malesuada fames ac ante ipsum primis in faucibus. Praesent molestie dolor non lobortis ornare. Duis quis nisl sollicitudin, accumsan ante sed, eleifend velit. Maecenas maximus sed ante nec auctor.", - "Donec vitae felis lectus. Aenean velit sapien, porttitor ut feugiat a, consectetur et risus. Proin ac viverra sem. Nullam sagittis ex tortor, eu pellentesque tellus efficitur at. Nunc non egestas leo. Nam sed suscipit neque. Nam sodales vel neque eget eleifend. Vivamus in condimentum elit, consectetur commodo ex. Suspendisse rutrum, sapien efficitur cursus sodales, dolor orci pulvinar mauris, eu fringilla leo ex id leo. Interdum et malesuada fames ac ante ipsum primis in faucibus. Proin rhoncus sapien massa, molestie vestibulum augue hendrerit nec. Aliquam malesuada varius sapien id accumsan. Duis blandit aliquet felis, nec pellentesque lacus tincidunt et. Cras sed ligula vel nisl laoreet sagittis. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Praesent tristique a neque aliquet aliquam.", - "Aliquam sed egestas felis. Maecenas sollicitudin nisl in sapien posuere vulputate. Suspendisse eleifend sem magna, interdum consectetur augue venenatis at. Vivamus ornare orci vel orci sodales maximus. Donec ultricies felis ac nulla fermentum gravida. Phasellus vulputate turpis odio, a varius nibh luctus et. Aliquam tincidunt, metus ut congue porttitor, nibh dui ullamcorper quam, a eleifend elit ipsum sit amet quam. Aenean venenatis mollis interdum. Nunc cursus ex sit amet enim lacinia hendrerit. Nullam at libero iaculis, consectetur velit in, porta sem. Ut mattis ut ex in imperdiet. Maecenas pellentesque sit amet dui eget vehicula. Sed posuere, arcu pretium convallis tincidunt, turpis leo dignissim felis, non euismod diam magna a risus. Suspendisse a arcu nec turpis pulvinar ullamcorper. Nunc iaculis malesuada elit eu pretium. Aenean a neque a sapien tincidunt faucibus.", - "Ut a eleifend augue, eget posuere augue. Proin purus neque, pretium condimentum ipsum ut, venenatis tincidunt nunc. In vitae odio in justo pharetra tincidunt. Maecenas vel tellus interdum, suscipit tellus sit amet, cursus justo. Mauris sollicitudin euismod molestie. Cras eros nisi, molestie vel elementum ut, consequat ac nunc. In consectetur nulla vitae interdum elementum. Praesent faucibus magna et iaculis congue. Curabitur convallis cursus porttitor. Praesent hendrerit justo ut sem convallis sollicitudin eu at odio." - }; - var blogPosts = new BlogPost[numberOfRecords]; - Random random = new Random(); - for (int i = 0; i < numberOfRecords; i++) - { - blogPosts[i] = new BlogPost() - { - Title = $"Blog post {i + 1}", - Content = contentStrings[random.Next(0, 5)], - PublicationDateTime = DateTime.UtcNow.AddDays(random.NextDouble() * -30) - }; - } - List = new ReadOnlyCollection(blogPosts); - } - } -} diff --git a/csharp/Program.cs b/csharp/Program.cs deleted file mode 100644 index 7b376eb..0000000 --- a/csharp/Program.cs +++ /dev/null @@ -1,101 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using BenchmarkDotNet.Configs; -using BenchmarkDotNet.Exporters; -using BenchmarkDotNet.Exporters.Json; -using BenchmarkDotNet.Jobs; -using BenchmarkDotNet.Reports; -using BenchmarkDotNet.Running; -using Comparisons.SQLiteVSDoublets.Doublets; -using Comparisons.SQLiteVSDoublets.Model; -using Comparisons.SQLiteVSDoublets.SQLite; - -namespace Comparisons.SQLiteVSDoublets; - -internal static class Program -{ - private static void Main(string[] args) - { - if (args.Length == 1 && args[0] == "--self-test") - { - LinksSelfTest.Run(); - return; - } - - if (args.Length == 1 && args[0] == "--manual-object-test") - { - RunManualObjectTest(); - return; - } - - if (args.Length == 0) - { - // Preserve the original entry point for the object-like benchmark. - BenchmarkRunner.Run(); - return; - } - - BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args, BenchmarkConfig()); - } - - private static void RunManualObjectTest() - { - const int numberOfTestRuns = 1; - const int numberOfRecordsPerTestRun = 1; - BlogPosts.GenerateData(numberOfRecordsPerTestRun); - var sqliteTestRuns = new List(); - var doubletsTestRuns = new List(); - for (var index = 0; index < numberOfTestRuns; index++) - { - var sqliteTestRun = new SQLiteTestRun("test.db"); - sqliteTestRun.Run(); - sqliteTestRuns.Add(sqliteTestRun); - var doubletsTestRun = new DoubletsTestRun("test.links"); - doubletsTestRun.Run(); - doubletsTestRuns.Add(doubletsTestRun); - } - Console.WriteLine("SQLite results:"); - Console.WriteLine(GetResultsAverage(sqliteTestRuns)); - Console.WriteLine("Doublets results:"); - Console.WriteLine(GetResultsAverage(doubletsTestRuns)); - } - - private static TestRunResults GetResultsAverage(IEnumerable testRuns) => new() - { - PrepareTime = new TimeSpan( - (long)testRuns.Select(run => run.Results.PrepareTime.Ticks).Average()), - DbSizeAfterPrepare = (long)testRuns.Select( - run => run.Results.DbSizeAfterPrepare).Average(), - ListCreationTime = new TimeSpan( - (long)testRuns.Select(run => run.Results.ListCreationTime.Ticks).Average()), - DbSizeAfterCreation = (long)testRuns.Select( - run => run.Results.DbSizeAfterCreation).Average(), - ListReadingTime = new TimeSpan( - (long)testRuns.Select(run => run.Results.ListReadingTime.Ticks).Average()), - DbSizeAfterReading = (long)testRuns.Select( - run => run.Results.DbSizeAfterReading).Average(), - ListDeletionTime = new TimeSpan( - (long)testRuns.Select(run => run.Results.ListDeletionTime.Ticks).Average()), - DbSizeAfterDeletion = (long)testRuns.Select( - run => run.Results.DbSizeAfterDeletion).Average(), - }; - - private static IConfig BenchmarkConfig() - { - var warmups = EnvironmentValue("BENCHMARK_WARMUP_COUNT", 1); - var iterations = EnvironmentValue("BENCHMARK_ITERATION_COUNT", 3); - var job = Job.Default - .WithWarmupCount(warmups) - .WithIterationCount(iterations); - return ManualConfig.Create(DefaultConfig.Instance) - .AddJob(job) - .AddExporter(JsonExporter.Full) - .WithSummaryStyle(SummaryStyle.Default.WithMaxParameterColumnWidth(40)); - } - - private static int EnvironmentValue(string name, int fallback) => - int.TryParse(Environment.GetEnvironmentVariable(name), out var value) && value > 0 - ? value - : fallback; -} diff --git a/csharp/SQLite/SQLiteDbContext.cs b/csharp/SQLite/SQLiteDbContext.cs deleted file mode 100644 index fdf6a74..0000000 --- a/csharp/SQLite/SQLiteDbContext.cs +++ /dev/null @@ -1,85 +0,0 @@ -using System.Reflection; -using Microsoft.EntityFrameworkCore; -using Comparisons.SQLiteVSDoublets.Model; - -namespace Comparisons.SQLiteVSDoublets.SQLite -{ - /// - /// - /// Represents the sq lite db context. - /// - /// - /// - /// - public class SQLiteDbContext : DbContext - { - /// - /// - /// The db filename. - /// - /// - /// - private readonly string _dbFilename; - - /// - /// - /// Gets or sets the blog posts value. - /// - /// - /// - public DbSet BlogPosts { get; set; } - - /// - /// - /// Initializes a new instance. - /// - /// - /// - /// - /// A db filename. - /// - /// - public SQLiteDbContext(string dbFilename) => _dbFilename = dbFilename; - - /// - /// - /// Ons the configuring using the specified options builder. - /// - /// - /// - /// - /// The options builder. - /// - /// - protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) - { - optionsBuilder.UseSqlite($"Filename={_dbFilename}", options => - { - options.MigrationsAssembly(Assembly.GetExecutingAssembly().FullName); - }); - base.OnConfiguring(optionsBuilder); - } - - /// - /// - /// Ons the model creating using the specified model builder. - /// - /// - /// - /// - /// The model builder. - /// - /// - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - modelBuilder.Entity().ToTable("BlogPosts", "test"); - modelBuilder.Entity(entity => - { - entity.HasKey(e => e.Id); - entity.HasIndex(e => e.Title).IsUnique(); - entity.Property(e => e.PublicationDateTime).HasDefaultValueSql("CURRENT_TIMESTAMP"); - }); - base.OnModelCreating(modelBuilder); - } - } -} diff --git a/csharp/SQLite/SQLiteTestRun.cs b/csharp/SQLite/SQLiteTestRun.cs deleted file mode 100644 index bfd7697..0000000 --- a/csharp/SQLite/SQLiteTestRun.cs +++ /dev/null @@ -1,81 +0,0 @@ -using System.Linq; -using Comparisons.SQLiteVSDoublets.Model; - -namespace Comparisons.SQLiteVSDoublets.SQLite -{ - /// - /// - /// Represents the sq lite test run. - /// - /// - /// - /// - public class SQLiteTestRun : TestRun - { - /// - /// - /// Initializes a new instance. - /// - /// - /// - /// - /// A db filename. - /// - /// - public SQLiteTestRun(string dbFilename) : base(dbFilename) { } - - /// - /// - /// Prepares this instance. - /// - /// - /// - public override void Prepare() - { - using var dbContext = new SQLiteDbContext(DbFilename); - dbContext.Database.EnsureCreated(); - } - - /// - /// - /// Creates the list. - /// - /// - /// - public override void CreateList() - { - using var dbContext = new SQLiteDbContext(DbFilename); - dbContext.BlogPosts.AddRange(BlogPosts.List); - dbContext.SaveChanges(); - } - - /// - /// - /// Reads the list. - /// - /// - /// - public override void ReadList() - { - using var dbContext = new SQLiteDbContext(DbFilename); - foreach (var blogPost in dbContext.BlogPosts) - { - ReadBlogPosts.Add(blogPost); - } - } - - /// - /// - /// Deletes the list. - /// - /// - /// - public override void DeleteList() - { - using var dbContext = new SQLiteDbContext(DbFilename); - var blogPostsToDelete = dbContext.BlogPosts.ToList(); - dbContext.BlogPosts.RemoveRange(blogPostsToDelete); - dbContext.SaveChanges(); - } - } -} diff --git a/csharp/SQLiteVSDoublets.Tests/SQLiteVSDoublets.Tests.csproj b/csharp/SQLiteVSDoublets.Tests/SQLiteVSDoublets.Tests.csproj new file mode 100644 index 0000000..23debdc --- /dev/null +++ b/csharp/SQLiteVSDoublets.Tests/SQLiteVSDoublets.Tests.csproj @@ -0,0 +1,20 @@ + + + + net10.0 + enable + enable + Comparisons.SQLiteVSDoublets.Tests + true + Exe + + + + + + + + + + + diff --git a/csharp/SQLiteVSDoublets.Tests/StoragesTests.cs b/csharp/SQLiteVSDoublets.Tests/StoragesTests.cs new file mode 100644 index 0000000..9316718 --- /dev/null +++ b/csharp/SQLiteVSDoublets.Tests/StoragesTests.cs @@ -0,0 +1,111 @@ +using Platform.Memory; +using Xunit; +using static Comparisons.SQLiteVSDoublets.Dataset; + +namespace Comparisons.SQLiteVSDoublets.Tests; + +public class StoragesTests +{ + private const ulong Size = 1_000; + + [Fact] + public void LinksAreUniqueAndReferenceCreatedIds() + { + var links = new HashSet<(ulong, ulong)>(); + for (ulong i = 1; i <= Size; i++) + { + var (from, to) = Link(i); + Assert.InRange(from, 1UL, i); + Assert.InRange(to, 1UL, i); + links.Add((from, to)); + } + Assert.Equal((int)Size, links.Count); + } + + [Theory] + [InlineData(1UL), InlineData(2UL), InlineData(3UL), InlineData(10UL), InlineData(1_000UL), InlineData(1_024UL), InlineData(65_536UL)] + public void ScatteredVisitsEveryIdOnce(ulong n) => + Assert.Equal(Enumerable.Range(1, (int)n).Select(i => (ulong)i), Scattered(n).Order()); + + public static TheoryData LinksVariants => [.. Harness.LinksVariants]; + + public static TheoryData ObjectsVariants => [.. Harness.ObjectsVariants]; + + [Theory, MemberData(nameof(LinksVariants))] + public void EveryLinksVariantPassesTheLifecycle(string variant) + { + var directory = Directory.CreateTempSubdirectory().FullName; + Harness.MeasureLinks(variant, Size, 1, directory); + Harness.MeasureLinks(variant, Size, 1, directory); + } + + [Theory, MemberData(nameof(ObjectsVariants))] + public void EveryObjectsVariantPassesTheLifecycle(string variant) + { + var directory = Directory.CreateTempSubdirectory().FullName; + Harness.MeasureObjects(variant, Size / 10, 1, directory); + Harness.MeasureObjects(variant, Size / 10, 1, directory); + } + + [Fact] + public void SplitStoreKeepsLinksUpdatedToReferenceThemselves() + { + using var links = new DoubletsLinks(Harness.Split(new HeapResizableDirectMemory(), new HeapResizableDirectMemory(), external: false)); + var point = links.Create(1, 1); + var link = links.Create(point, 2); + links.Update(point, point, point); + links.Update(link, link, point); + Assert.Equal(new Link(link, link, point), links.Get(link)); + links.Delete(link); + links.Delete(point); + Assert.Equal(0UL, links.Count()); + } + + private sealed class Lossy(SQLiteLinks links) : ILinksStorage + { + public uint Create(uint from, uint to) => links.Create(from, to); + public void Update(uint id, uint from, uint to) => links.Update(id, from, to); + public void Delete(uint id) => links.Delete(id); + public Link? Get(uint id) => links.Get(id); + public uint? Search(uint from, uint to) => links.Search(from, to); + public void Each(Action> visit) => links.EachWithFrom(1, visit); + public void EachWithFrom(uint from, Action> visit) => links.EachWithFrom(from, visit); + public void EachWithTo(uint to, Action> visit) => links.EachWithTo(to, visit); + public ulong Count() => links.Count(); + public void Dispose() => links.Dispose(); + } + + [Fact] + public void LifecycleRejectsAStorageThatLosesRecords() + { + using var lossy = new Lossy(SQLiteLinks.InMemory()); + var error = Assert.Throws(() => Harness.LinksLifecycle(lossy, Size, () => { })); + Assert.StartsWith("query_all returned unexpected records", error.Message); + } + + private static void RoundTrip(IBlogPostsStorage posts) where T : struct + { + using var _ = posts; + var inputs = Enumerable.Range(1, 20).Select(i => BlogPost((ulong)i)).ToList(); + inputs.Add(new BlogPost("Ünïcödé 🌍 ✓", "a", 0)); + inputs.Add(new BlogPost("", "ab", uint.MaxValue / 2)); + var ids = inputs.Select(posts.Create).ToList(); + Assert.Equal(inputs, ids.Select(id => posts.Get(id))); + var stored = new List<(T, BlogPost)>(); + posts.Each((id, post) => stored.Add((id, post))); + Assert.Equal(ids.Zip(inputs), stored.OrderBy(entry => entry.Item1)); + ids.ForEach(posts.Delete); + Assert.Equal(0UL, posts.Count()); + } + + [Theory, InlineData(false), InlineData(true)] + public void EveryObjectsStorageReturnsTheStoredPosts(bool cached) + { + RoundTrip(SQLiteBlogPosts.InMemory()); + RoundTrip(SQLiteBlogPosts.InMemory()); + RoundTrip(new DoubletsBlogPosts(Harness.United(new HeapResizableDirectMemory(), external: true), cached)); + RoundTrip(new DoubletsBlogPosts(Harness.United(new HeapResizableDirectMemory(), external: true), cached)); + RoundTrip(new DoubletsBlogPosts(Harness.Split(new HeapResizableDirectMemory(), new HeapResizableDirectMemory(), external: true), cached)); + RoundTrip(new DoubletsBlogPosts(Harness.Split(new HeapResizableDirectMemory(), new HeapResizableDirectMemory(), external: true), cached)); + } +} diff --git a/csharp/SQLiteVSDoublets.slnx b/csharp/SQLiteVSDoublets.slnx new file mode 100644 index 0000000..2ab0b4a --- /dev/null +++ b/csharp/SQLiteVSDoublets.slnx @@ -0,0 +1,4 @@ + + + + diff --git a/csharp/SQLiteVSDoublets/Dataset.cs b/csharp/SQLiteVSDoublets/Dataset.cs new file mode 100644 index 0000000..7b04fc8 --- /dev/null +++ b/csharp/SQLiteVSDoublets/Dataset.cs @@ -0,0 +1,123 @@ +using System.Text; + +namespace Comparisons.SQLiteVSDoublets; + +/// Deterministic data shared by every storage and by the Rust implementation. +public static class Dataset +{ + private const ulong GoldenGamma = 0x9E37_79B9_7F4A_7C15; + + public static ulong SplitMix64(ulong x) + { + var z = x + GoldenGamma; + z = (z ^ (z >> 30)) * 0xBF58_476D_1CE4_E5B9; + z = (z ^ (z >> 27)) * 0x94D0_49BB_1331_11EB; + return z ^ (z >> 31); + } + + /// + /// The (From, To) pair of the link with id : both reference already created ids + /// and From + To == i + 1, so every pair is unique. + /// + public static (ulong From, ulong To) Link(ulong i) + { + var from = 1 + (SplitMix64(2 * i) % i); + return (from, i + 1 - from); + } + + /// + /// Order-sensitive mix of two numbers; summing mixes detects records that are lost, + /// duplicated or associated with the wrong id. + /// + public static ulong Mix(ulong a, ulong b) => ((a * GoldenGamma) ^ b) * GoldenGamma; + + public static ulong LinkChecksum(ulong id, ulong from, ulong to) => Mix(Mix(id, from), to); + + public static ulong LinksChecksum(ulong n, bool swapped) + { + ulong checksum = 0; + for (ulong i = 1; i <= n; i++) + { + var (from, to) = Link(i); + checksum += LinkChecksum(i, swapped ? to : from, swapped ? from : to); + } + return checksum; + } + + /// + /// Visits 1..=n once each in a scattered order, so point operations do not hit + /// neighbouring records one after another. + /// + public static IEnumerable Scattered(ulong n) + { + var stride = GoldenGamma % Math.Max(n, 1); + while (Gcd(stride, n) != 1) + { + stride++; + } + for (ulong k = 0; k < n; k++) + { + yield return 1 + ((k * stride) % n); + } + } + + private static ulong Gcd(ulong a, ulong b) => b == 0 ? a : Gcd(b, a % b); + + private const ulong September2020 = 1_600_000_000; + private const ulong ThirtyDays = 30 * 24 * 60 * 60; + + private static readonly string[] Paragraphs = + [ + "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis malesuada blandit mauris nec bibendum. Phasellus feugiat vehicula mauris et aliquet. Integer et gravida velit, in rutrum leo. Duis pretium, nunc ac posuere porttitor, augue sapien commodo tortor, nec consequat lorem eros ultricies odio. Aliquam varius congue ex nec viverra. Pellentesque eu velit tellus. Donec ac luctus nisi. Curabitur dignissim sodales mauris eu semper. Ut pretium lorem nulla, sit amet auctor arcu placerat vitae. Quisque lacinia dolor et consectetur fermentum. Nam ac orci vitae nulla aliquam tempor ac a nibh. Ut ac tincidunt lacus. Morbi vitae felis lorem.", + "Curabitur tincidunt nibh sit amet finibus dictum. Suspendisse aliquet arcu non rutrum ultrices. Integer ullamcorper mauris sit amet nibh aliquam, et tempor turpis hendrerit. In molestie elit et mauris rutrum, non auctor ligula ultricies. Vestibulum dignissim mauris finibus libero interdum hendrerit. Nunc vitae ipsum porttitor, egestas magna ut, sagittis sem. Donec euismod ac tortor vel porta. Vivamus convallis, ex at vestibulum rutrum, velit purus venenatis metus, sit amet aliquam sapien nibh quis elit. Aenean id neque a orci sodales venenatis. Integer ut orci ligula. Interdum et malesuada fames ac ante ipsum primis in faucibus. Praesent molestie dolor non lobortis ornare. Duis quis nisl sollicitudin, accumsan ante sed, eleifend velit. Maecenas maximus sed ante nec auctor.", + "Donec vitae felis lectus. Aenean velit sapien, porttitor ut feugiat a, consectetur et risus. Proin ac viverra sem. Nullam sagittis ex tortor, eu pellentesque tellus efficitur at. Nunc non egestas leo. Nam sed suscipit neque. Nam sodales vel neque eget eleifend. Vivamus in condimentum elit, consectetur commodo ex. Suspendisse rutrum, sapien efficitur cursus sodales, dolor orci pulvinar mauris, eu fringilla leo ex id leo. Interdum et malesuada fames ac ante ipsum primis in faucibus. Proin rhoncus sapien massa, molestie vestibulum augue hendrerit nec. Aliquam malesuada varius sapien id accumsan. Duis blandit aliquet felis, nec pellentesque lacus tincidunt et. Cras sed ligula vel nisl laoreet sagittis. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Praesent tristique a neque aliquet aliquam.", + "Aliquam sed egestas felis. Maecenas sollicitudin nisl in sapien posuere vulputate. Suspendisse eleifend sem magna, interdum consectetur augue venenatis at. Vivamus ornare orci vel orci sodales maximus. Donec ultricies felis ac nulla fermentum gravida. Phasellus vulputate turpis odio, a varius nibh luctus et. Aliquam tincidunt, metus ut congue porttitor, nibh dui ullamcorper quam, a eleifend elit ipsum sit amet quam. Aenean venenatis mollis interdum. Nunc cursus ex sit amet enim lacinia hendrerit. Nullam at libero iaculis, consectetur velit in, porta sem. Ut mattis ut ex in imperdiet. Maecenas pellentesque sit amet dui eget vehicula. Sed posuere, arcu pretium convallis tincidunt, turpis leo dignissim felis, non euismod diam magna a risus. Suspendisse a arcu nec turpis pulvinar ullamcorper. Nunc iaculis malesuada elit eu pretium. Aenean a neque a sapien tincidunt faucibus.", + "Ut a eleifend augue, eget posuere augue. Proin purus neque, pretium condimentum ipsum ut, venenatis tincidunt nunc. In vitae odio in justo pharetra tincidunt. Maecenas vel tellus interdum, suscipit tellus sit amet, cursus justo. Mauris sollicitudin euismod molestie. Cras eros nisi, molestie vel elementum ut, consequat ac nunc. In consectetur nulla vitae interdum elementum. Praesent faucibus magna et iaculis congue. Curabitur convallis cursus porttitor. Praesent hendrerit justo ut sem convallis sollicitudin eu at odio.", + ]; + + public static BlogPost BlogPost(ulong i) => + new($"Blog post {i}", Paragraphs[i % 5], September2020 + (SplitMix64(i) % ThirtyDays)); + + public static ulong BlogPostsChecksum(ulong n) + { + ulong checksum = 0; + for (ulong i = 1; i <= n; i++) + { + checksum += BlogPost(i).Checksum; + } + return checksum; + } + + /// + /// Like , but also checks that the i-th created id returns the i-th post. + /// + public static ulong BlogPostsByNumberChecksum(ulong n) + { + ulong checksum = 0; + for (ulong i = 1; i <= n; i++) + { + checksum += Mix(i, BlogPost(i).Checksum); + } + return checksum; + } +} + +/// Seconds since the Unix epoch. +public sealed record BlogPost(string Title, string Content, ulong PublicationDate) +{ + public ulong Checksum => Bytes(Title) + Bytes(Content) + PublicationDate; + + private static ulong Bytes(string text) + { + ulong sum = 0; + Span utf8 = stackalloc byte[4]; + foreach (var rune in text.EnumerateRunes()) + { + foreach (var b in utf8[..rune.EncodeToUtf8(utf8)]) + { + sum += b; + } + } + return sum; + } +} diff --git a/csharp/SQLiteVSDoublets/Harness.cs b/csharp/SQLiteVSDoublets/Harness.cs new file mode 100644 index 0000000..8d16019 --- /dev/null +++ b/csharp/SQLiteVSDoublets/Harness.cs @@ -0,0 +1,356 @@ +using System.Diagnostics; +using System.Numerics; +using System.Text.Json.Nodes; +using Platform.Data; +using Platform.Data.Doublets; +using Platform.Data.Doublets.Memory; +using Platform.Data.Doublets.Memory.Split.Generic; +using Platform.Data.Doublets.Memory.United.Generic; +using Platform.Memory; +using static Comparisons.SQLiteVSDoublets.Dataset; + +namespace Comparisons.SQLiteVSDoublets; + +/// +/// Runs every storage through the same lifecycle on a fresh store per repetition: +/// a timed batch per operation, each validated against the expected count and checksum. +/// +public static class Harness +{ + public static readonly string[] LinksVariants = + [ + "SQLite_Memory", + "SQLite_File", + "Doublets_United_Volatile", + "Doublets_United_NonVolatile", + "Doublets_Split_Volatile", + "Doublets_Split_NonVolatile", + ]; + + public static readonly string[] ObjectsVariants = + [ + "SQLite_Memory", + "SQLite_File", + "Doublets_United_Volatile_Cached", + "Doublets_United_Volatile_Uncached", + "Doublets_United_NonVolatile_Cached", + "Doublets_United_NonVolatile_Uncached", + "Doublets_Split_Volatile_Cached", + "Doublets_Split_Volatile_Uncached", + "Doublets_Split_NonVolatile_Cached", + "Doublets_Split_NonVolatile_Uncached", + ]; + + public static ulong WarmUpSize => 10_000; + + public static TimeSpan WarmUpTime => TimeSpan.FromSeconds(1); + + /// Count and order-sensitive checksum of the records an operation has seen. + public record struct Tally(ulong Count, ulong Checksum) + { + public void Add(ulong checksum) + { + Count++; + Checksum += checksum; + } + + public void AddLink(Link link) where T : IBinaryInteger => + Add(LinkChecksum(ulong.CreateChecked(link.Id), ulong.CreateChecked(link.From), ulong.CreateChecked(link.To))); + } + + private static void Timed(List<(string, TimeSpan)> timings, string operation, Tally expected, Func work) + { + var stopwatch = Stopwatch.StartNew(); + var actual = work(); + timings.Add((operation, stopwatch.Elapsed)); + Check(actual == expected, $"{operation} returned unexpected records: {actual} instead of {expected}"); + } + + private static void Check(bool condition, string message) + { + if (!condition) + { + throw new InvalidOperationException(message); + } + } + + public static List<(string, TimeSpan)> LinksLifecycle(ILinksStorage storage, ulong n, Action created) + where T : struct, IBinaryInteger + { + static T Int(ulong value) => T.CreateChecked(value); + var expected = new Tally(n, LinksChecksum(n, false)); + var timings = new List<(string, TimeSpan)>(); + + Timed(timings, "create", expected, () => storage.Transaction(() => + { + var tally = new Tally(); + for (ulong i = 1; i <= n; i++) + { + var (from, to) = Link(i); + var id = storage.Create(Int(from), Int(to)); + tally.AddLink(new Link(id, Int(from), Int(to))); + } + return tally; + })); + created(); + + Timed(timings, "query_all", expected, () => storage.Transaction(() => + { + var tally = new Tally(); + storage.Each(link => tally.AddLink(link)); + return tally; + })); + Timed(timings, "query_by_id", expected, () => storage.Transaction(() => + { + var tally = new Tally(); + foreach (var id in Scattered(n)) + { + tally.AddLink(storage.Get(Int(id))!.Value); + } + return tally; + })); + Timed(timings, "query_by_from_to", expected, () => storage.Transaction(() => + { + var tally = new Tally(); + foreach (var i in Scattered(n)) + { + var (from, to) = Link(i); + var id = storage.Search(Int(from), Int(to))!.Value; + tally.AddLink(new Link(id, Int(from), Int(to))); + } + return tally; + })); + Timed(timings, "query_by_from", expected, () => storage.Transaction(() => + { + var tally = new Tally(); + foreach (var from in Scattered(n)) + { + storage.EachWithFrom(Int(from), link => tally.AddLink(link)); + } + return tally; + })); + Timed(timings, "query_by_to", expected, () => storage.Transaction(() => + { + var tally = new Tally(); + foreach (var to in Scattered(n)) + { + storage.EachWithTo(Int(to), link => tally.AddLink(link)); + } + return tally; + })); + + var swapped = new Tally(n, LinksChecksum(n, true)); + Timed(timings, "update", swapped, () => storage.Transaction(() => + { + var tally = new Tally(); + foreach (var id in Scattered(n)) + { + var (newTo, newFrom) = Link(id); + storage.Update(Int(id), Int(newFrom), Int(newTo)); + tally.Add(LinkChecksum(id, newFrom, newTo)); + } + return tally; + })); + var stored = new Tally(); + storage.Each(link => stored.AddLink(link)); + Check(stored == swapped, "update did not swap every link"); + + Timed(timings, "delete", new Tally(n, 0), () => storage.Transaction(() => + { + var tally = new Tally(); + foreach (var id in Scattered(n)) + { + storage.Delete(Int(id)); + tally.Add(0); + } + return tally; + })); + Check(storage.Count() == 0, "delete left links behind"); + return timings; + } + + public static List<(string, TimeSpan)> ObjectsLifecycle(IBlogPostsStorage storage, ulong n, Action created) + where T : struct, IBinaryInteger + { + var expected = new Tally(n, BlogPostsChecksum(n)); + var timings = new List<(string, TimeSpan)>(); + var ids = new List((int)n); + + Timed(timings, "create", expected, () => storage.Transaction(() => + { + var tally = new Tally(); + for (ulong i = 1; i <= n; i++) + { + var post = BlogPost(i); + ids.Add(storage.Create(post)); + tally.Add(post.Checksum); + } + return tally; + })); + created(); + + Timed(timings, "read_all", expected, () => storage.Transaction(() => + { + var tally = new Tally(); + storage.Each((_, post) => tally.Add(post.Checksum)); + return tally; + })); + Timed(timings, "read_by_id", new Tally(n, BlogPostsByNumberChecksum(n)), () => storage.Transaction(() => + { + var tally = new Tally(); + foreach (var i in Scattered(n)) + { + var post = storage.Get(ids[(int)i - 1])!; + tally.Add(Mix(i, post.Checksum)); + } + return tally; + })); + Timed(timings, "delete", new Tally(n, 0), () => storage.Transaction(() => + { + var tally = new Tally(); + foreach (var i in Scattered(n)) + { + storage.Delete(ids[(int)i - 1]); + tally.Add(0); + } + return tally; + })); + Check(storage.Count() == 0, "delete left blog posts behind"); + return timings; + } + + public sealed record Measurement(string Variant, ulong? FileBytes, List<(string Operation, List Samples)> Operations) + { + public JsonObject ToJson() + { + var operations = new JsonObject(); + foreach (var (operation, samples) in Operations) + { + var sorted = samples.Order().ToList(); + var middle = sorted.Count / 2; + var median = sorted.Count % 2 == 0 ? (sorted[middle - 1] + sorted[middle]) / 2 : sorted[middle]; + operations[operation] = new JsonObject + { + ["median_ns"] = median, + ["min_ns"] = sorted[0], + ["max_ns"] = sorted[^1], + ["samples_ns"] = new JsonArray(samples.Select(sample => (JsonNode)sample).ToArray()), + }; + } + return new JsonObject { ["variant"] = Variant, ["file_bytes"] = FileBytes, ["operations"] = operations }; + } + } + + private static ulong DirectoryBytes(string directory) => + (ulong)new DirectoryInfo(directory).GetFiles().Sum(file => file.Length); + + /// + /// Times on fresh stores opened in empty directories, + /// after discarded warm-ups that run for at least , so the JIT has optimized the code. + /// + private static Measurement Measure( + string variant, + ulong n, + int repetitions, + string directory, + Func open, + Func> lifecycle) + where TStorage : IDisposable + { + (List<(string, TimeSpan)>, ulong) Run(ulong size) + { + var workspace = Path.Combine(directory, variant); + Directory.CreateDirectory(workspace); + GC.Collect(); + GC.WaitForPendingFinalizers(); + ulong fileBytes = 0; + List<(string, TimeSpan)> timings; + using (var storage = open(workspace)) + { + timings = lifecycle(storage, size, () => fileBytes = DirectoryBytes(workspace)); + } + Directory.Delete(workspace, true); + return (timings, fileBytes); + } + + var warmUp = Stopwatch.StartNew(); + do + { + Run(Math.Min(n, WarmUpSize)); + } + while (warmUp.Elapsed < WarmUpTime); + var measurement = new Measurement(variant, null, []); + for (var repetition = 1; repetition <= repetitions; repetition++) + { + var (timings, fileBytes) = Run(n); + Console.Error.WriteLine($"{variant} #{repetition}: {Summary(timings, n)}"); + measurement = measurement with { FileBytes = fileBytes > 0 ? fileBytes : null }; + foreach (var (operation, elapsed) in timings) + { + var nsPerOperation = elapsed.TotalNanoseconds / n; + var index = measurement.Operations.FindIndex(entry => entry.Operation == operation); + if (index < 0) + { + measurement.Operations.Add((operation, [nsPerOperation])); + } + else + { + measurement.Operations[index].Samples.Add(nsPerOperation); + } + } + } + return measurement; + } + + private static string Summary(List<(string Operation, TimeSpan Elapsed)> timings, ulong n) => + string.Join(", ", timings.Select(timing => $"{timing.Operation} {timing.Elapsed.TotalNanoseconds / n:F0} ns/op")); + + private static string File(string directory, string name) => Path.Combine(directory, name); + + private static FileMappedResizableDirectMemory Mapped(string directory, string name) => new(File(directory, name)); + + /// Objects store numbers and Unicode symbols as raw numbers, which are external references. + /// + /// The size balanced index trees (the default) degenerate when many links share a source or a target, + /// which makes every blog post creation linear in the number of stored posts; + /// the AVL trees stay logarithmic (see experiments/csharp_objects_profile). + /// + public static ILinks United(IResizableDirectMemory memory, bool external) where T : struct, IBinaryInteger, IUnsignedNumber, IMinMaxValue => + new UnitedMemoryLinks(memory, UnitedMemoryLinks.DefaultLinksSizeStep, new LinksConstants(external), IndexTreeType.SizedAndThreadedAVLBalancedTree); + + public static ILinks Split(IResizableDirectMemory data, IResizableDirectMemory index, bool external) where T : struct, IBinaryInteger, IUnsignedNumber, IMinMaxValue => + new SplitMemoryLinks(data, index, SplitMemoryLinks.DefaultLinksSizeStep, new LinksConstants(external)); + + public static Measurement MeasureLinks(string variant, ulong n, int repetitions, string directory) + where T : struct, IBinaryInteger, IUnsignedNumber, IMinMaxValue + { + Func> open = variant switch + { + "SQLite_Memory" => _ => SQLiteLinks.InMemory(), + "SQLite_File" => dir => SQLiteLinks.Open(File(dir, "links.db")), + "Doublets_United_Volatile" => _ => new DoubletsLinks(United(new HeapResizableDirectMemory(), external: false)), + "Doublets_United_NonVolatile" => dir => new DoubletsLinks(United(Mapped(dir, "links.links"), external: false)), + "Doublets_Split_Volatile" => _ => new DoubletsLinks(Split(new HeapResizableDirectMemory(), new HeapResizableDirectMemory(), external: false)), + "Doublets_Split_NonVolatile" => dir => new DoubletsLinks(Split(Mapped(dir, "data.links"), Mapped(dir, "index.links"), external: false)), + _ => throw new ArgumentException($"unknown links variant {variant}"), + }; + return Measure(variant, n, repetitions, directory, open, LinksLifecycle); + } + + public static Measurement MeasureObjects(string variant, ulong n, int repetitions, string directory) + where T : struct, IBinaryInteger, IUnsignedNumber, IMinMaxValue + { + var cached = variant.EndsWith("_Cached"); + Func> open = variant.Replace("_Uncached", "").Replace("_Cached", "") switch + { + "SQLite_Memory" => _ => SQLiteBlogPosts.InMemory(), + "SQLite_File" => dir => SQLiteBlogPosts.Open(File(dir, "blog_posts.db")), + "Doublets_United_Volatile" => _ => new DoubletsBlogPosts(United(new HeapResizableDirectMemory(), external: true), cached), + "Doublets_United_NonVolatile" => dir => new DoubletsBlogPosts(United(Mapped(dir, "links.links"), external: true), cached), + "Doublets_Split_Volatile" => _ => new DoubletsBlogPosts(Split(new HeapResizableDirectMemory(), new HeapResizableDirectMemory(), external: true), cached), + "Doublets_Split_NonVolatile" => dir => new DoubletsBlogPosts(Split(Mapped(dir, "data.links"), Mapped(dir, "index.links"), external: true), cached), + _ => throw new ArgumentException($"unknown objects variant {variant}"), + }; + return Measure(variant, n, repetitions, directory, open, ObjectsLifecycle); + } +} diff --git a/csharp/SQLiteVSDoublets/Links.cs b/csharp/SQLiteVSDoublets/Links.cs new file mode 100644 index 0000000..ee76e5e --- /dev/null +++ b/csharp/SQLiteVSDoublets/Links.cs @@ -0,0 +1,138 @@ +using System.Numerics; +using Microsoft.Data.Sqlite; +using Platform.Data; +using Platform.Data.Doublets; + +namespace Comparisons.SQLiteVSDoublets; + +public readonly record struct Link(T Id, T From, T To); + +public interface ILinksStorage : IDisposable where T : struct +{ + T Create(T from, T to); + void Update(T id, T from, T to); + void Delete(T id); + Link? Get(T id); + T? Search(T from, T to); + void Each(Action> visit); + void EachWithFrom(T from, Action> visit); + void EachWithTo(T to, Action> visit); + ulong Count(); + + TResult Transaction(Func work) => work(); +} + +public sealed class SQLiteLinks : SQLiteStorage, ILinksStorage where T : struct, IBinaryInteger +{ + private static readonly (string, SqliteType) IdParameter = ("$id", SqliteType.Integer); + private static readonly (string, SqliteType) FromParameter = ("$from", SqliteType.Integer); + private static readonly (string, SqliteType) ToParameter = ("$to", SqliteType.Integer); + + private readonly SqliteCommand _create; + private readonly SqliteCommand _update; + private readonly SqliteCommand _delete; + private readonly SqliteCommand _get; + private readonly SqliteCommand _search; + private readonly SqliteCommand _each; + private readonly SqliteCommand _eachWithFrom; + private readonly SqliteCommand _eachWithTo; + + public static SQLiteLinks Open(string path) => new(path); + + public static SQLiteLinks InMemory() => new(":memory:"); + + private SQLiteLinks(string path) : base(path, "links", """ + CREATE TABLE links (id INTEGER PRIMARY KEY, "from" INTEGER NOT NULL, "to" INTEGER NOT NULL); + CREATE INDEX links_from_to ON links ("from", "to"); + CREATE INDEX links_to_from ON links ("to", "from"); + """) + { + _create = Command("""INSERT INTO links ("from", "to") VALUES ($from, $to)""", FromParameter, ToParameter); + _update = Command("""UPDATE links SET "from" = $from, "to" = $to WHERE id = $id""", IdParameter, FromParameter, ToParameter); + _delete = Command("DELETE FROM links WHERE id = $id", IdParameter); + _get = Command("""SELECT "from", "to" FROM links WHERE id = $id""", IdParameter); + _search = Command("""SELECT id FROM links WHERE "from" = $from AND "to" = $to""", FromParameter, ToParameter); + _each = Command("""SELECT id, "from", "to" FROM links"""); + _eachWithFrom = Command("""SELECT id, "from", "to" FROM links WHERE "from" = $from""", FromParameter); + _eachWithTo = Command("""SELECT id, "from", "to" FROM links WHERE "to" = $to""", ToParameter); + } + + private static T Id(long sql) => T.CreateChecked(sql); + + private static SqliteCommand With(SqliteCommand command, params ReadOnlySpan values) + { + for (var i = 0; i < values.Length; i++) + { + command.Parameters[i].Value = long.CreateChecked(values[i]); + } + return command; + } + + public T Create(T from, T to) + { + With(_create, from, to).ExecuteNonQuery(); + return Id(LastInsertRowId); + } + + public void Update(T id, T from, T to) => With(_update, id, from, to).ExecuteNonQuery(); + + public void Delete(T id) => With(_delete, id).ExecuteNonQuery(); + + public Link? Get(T id) + { + using var reader = With(_get, id).ExecuteReader(); + return reader.Read() ? new Link(id, Id(reader.GetInt64(0)), Id(reader.GetInt64(1))) : null; + } + + public T? Search(T from, T to) => With(_search, from, to).ExecuteScalar() is long id ? Id(id) : null; + + public void Each(Action> visit) => Read(_each, visit); + + public void EachWithFrom(T from, Action> visit) => Read(With(_eachWithFrom, from), visit); + + public void EachWithTo(T to, Action> visit) => Read(With(_eachWithTo, to), visit); + + private static void Read(SqliteCommand command, Action> visit) + { + using var reader = command.ExecuteReader(); + while (reader.Read()) + { + visit(new Link(Id(reader.GetInt64(0)), Id(reader.GetInt64(1)), Id(reader.GetInt64(2)))); + } + } +} + +public sealed class DoubletsLinks(ILinks links) : ILinksStorage + where T : struct, IUnsignedNumber, IComparisonOperators +{ + private readonly T _any = links.Constants.Any; + private readonly T _continue = links.Constants.Continue; + + public T Create(T from, T to) => links.CreateAndUpdate(from, to); + + public void Update(T id, T from, T to) => links.Update(id, from, to); + + public void Delete(T id) => links.Delete(id, handler: null); + + public Link? Get(T id) => links.Exists(id) ? Link(links.GetLink(id)!) : null; + + public T? Search(T from, T to) => links.SearchOrDefault(from, to) is var id && id != T.Zero ? id : null; + + public void Each(Action> visit) => Each(visit, _any, _any, _any); + + public void EachWithFrom(T from, Action> visit) => Each(visit, _any, from, _any); + + public void EachWithTo(T to, Action> visit) => Each(visit, _any, _any, to); + + private void Each(Action> visit, params T[] query) => links.Each(link => + { + visit(Link(link!)); + return _continue; + }, query); + + private static Link Link(IList values) => new(values[0], values[1], values[2]); + + public ulong Count() => ulong.CreateChecked(links.Count()); + + public void Dispose() => (links as IDisposable)?.Dispose(); +} diff --git a/csharp/SQLiteVSDoublets/Objects.cs b/csharp/SQLiteVSDoublets/Objects.cs new file mode 100644 index 0000000..bd9045d --- /dev/null +++ b/csharp/SQLiteVSDoublets/Objects.cs @@ -0,0 +1,189 @@ +using System.Numerics; +using Microsoft.Data.Sqlite; +using Platform.Collections.Stacks; +using Platform.Converters; +using Platform.Data; +using Platform.Data.Doublets; +using Platform.Data.Doublets.CriterionMatchers; +using Platform.Data.Doublets.PropertyOperators; +using Platform.Data.Doublets.Sequences.Converters; +using Platform.Data.Doublets.Sequences.Unicode; +using Platform.Data.Doublets.Sequences.Walkers; +using Platform.Data.Numbers.Raw; + +namespace Comparisons.SQLiteVSDoublets; + +public interface IBlogPostsStorage : IDisposable where T : struct +{ + T Create(BlogPost post); + BlogPost? Get(T id); + void Each(Action visit); + void Delete(T id); + ulong Count(); + + TResult Transaction(Func work) => work(); +} + +public sealed class SQLiteBlogPosts : SQLiteStorage, IBlogPostsStorage where T : struct, IBinaryInteger +{ + private readonly SqliteCommand _create; + private readonly SqliteCommand _get; + private readonly SqliteCommand _each; + private readonly SqliteCommand _delete; + + public static SQLiteBlogPosts Open(string path) => new(path); + + public static SQLiteBlogPosts InMemory() => new(":memory:"); + + private SQLiteBlogPosts(string path) : base(path, "blog_posts", + "CREATE TABLE blog_posts (id INTEGER PRIMARY KEY, title TEXT NOT NULL, content TEXT NOT NULL, publication_date INTEGER NOT NULL)") + { + _create = Command( + "INSERT INTO blog_posts (title, content, publication_date) VALUES ($title, $content, $publication_date)", + ("$title", SqliteType.Text), ("$content", SqliteType.Text), ("$publication_date", SqliteType.Integer)); + _get = Command("SELECT title, content, publication_date FROM blog_posts WHERE id = $id", ("$id", SqliteType.Integer)); + _each = Command("SELECT title, content, publication_date, id FROM blog_posts"); + _delete = Command("DELETE FROM blog_posts WHERE id = $id", ("$id", SqliteType.Integer)); + } + + private static BlogPost BlogPost(SqliteDataReader reader) => + new(reader.GetString(0), reader.GetString(1), (ulong)reader.GetInt64(2)); + + public T Create(BlogPost post) + { + _create.Parameters[0].Value = post.Title; + _create.Parameters[1].Value = post.Content; + _create.Parameters[2].Value = (long)post.PublicationDate; + _create.ExecuteNonQuery(); + return T.CreateChecked(LastInsertRowId); + } + + public BlogPost? Get(T id) + { + _get.Parameters[0].Value = long.CreateChecked(id); + using var reader = _get.ExecuteReader(); + return reader.Read() ? BlogPost(reader) : null; + } + + public void Each(Action visit) + { + using var reader = _each.ExecuteReader(); + while (reader.Read()) + { + visit(T.CreateChecked(reader.GetInt64(3)), BlogPost(reader)); + } + } + + public void Delete(T id) + { + _delete.Parameters[0].Value = long.CreateChecked(id); + _delete.ExecuteNonQuery(); + } +} + +/// +/// Stores each blog post as a (blog_post, itself) link with (post, property) -> value properties, +/// strings as balanced-variant sequences of Unicode symbols and dates as raw numbers. +/// +public sealed class DoubletsBlogPosts : IBlogPostsStorage + where T : struct, IUnsignedNumber, IComparisonOperators +{ + private readonly ILinks _links; + private readonly T _any; + private readonly T _title; + private readonly T _content; + private readonly T _publicationDate; + private readonly T _blogPost; + private readonly PropertiesOperator _properties; + private readonly AddressToRawNumberConverter _addressToNumber = new(); + private readonly RawNumberToAddressConverter _numberToAddress = new(); + private readonly IConverter _stringToSequence; + private readonly IConverter _sequenceToString; + + public DoubletsBlogPosts(ILinks links, bool cacheSequences) + { + _links = links; + _any = links.Constants.Any; + var meaningRoot = links.CreatePoint(); + T Marker() + { + var marker = links.Create(); + return links.Update(marker, meaningRoot, marker); + } + var unicodeSymbol = Marker(); + var unicodeSequence = Marker(); + _title = Marker(); + _content = Marker(); + _publicationDate = Marker(); + _blogPost = Marker(); + _properties = new PropertiesOperator(links); + + var unicodeSymbolMatcher = new TargetMatcher(links, unicodeSymbol); + IConverter stringToSequence = new StringToUnicodeSequenceConverter( + links, + new CharToUnicodeSymbolConverter(links, _addressToNumber, unicodeSymbol), + new BalancedVariantConverter(links), + unicodeSequence); + IConverter sequenceToString = new UnicodeSequenceToStringConverter( + links, + new RightSequenceWalker(links, new DefaultStack(), unicodeSymbolMatcher.IsMatched), + new UnicodeSymbolToCharConverter(links, _numberToAddress, unicodeSymbolMatcher), + unicodeSequence); + _stringToSequence = cacheSequences ? new CachingConverterDecorator(stringToSequence) : stringToSequence; + _sequenceToString = cacheSequences ? new CachingConverterDecorator(sequenceToString) : sequenceToString; + } + + public T Create(BlogPost post) + { + var blogPost = _links.Create(); + _links.Update(blogPost, _blogPost, blogPost); + _properties.SetValue(blogPost, _title, _stringToSequence.Convert(post.Title)); + _properties.SetValue(blogPost, _content, _stringToSequence.Convert(post.Content)); + _properties.SetValue(blogPost, _publicationDate, _addressToNumber.Convert(T.CreateChecked(post.PublicationDate))); + return blogPost; + } + + public BlogPost? Get(T id) + { + var title = _properties.GetValue(id, _title); + var content = _properties.GetValue(id, _content); + var publicationDate = _properties.GetValue(id, _publicationDate); + if (title == T.Zero || content == T.Zero || publicationDate == T.Zero) + { + return null; + } + return new BlogPost( + _sequenceToString.Convert(title), + _sequenceToString.Convert(content), + ulong.CreateChecked(_numberToAddress.Convert(publicationDate))); + } + + public void Each(Action visit) + { + var ids = new List(); + _links.Each(post => + { + ids.Add(post![0]); + return _links.Constants.Continue; + }, _any, _blogPost, _any); + foreach (var id in ids) + { + visit(id, Get(id)!); + } + } + + public void Delete(T id) + { + foreach (var property in (ReadOnlySpan)[_title, _content, _publicationDate]) + { + var objectProperty = _links.SearchOrDefault(id, property); + _links.Delete(_links.SingleOrDefault([_any, objectProperty, _any])![0], handler: null); + _links.Delete(objectProperty, handler: null); + } + _links.Delete(id, handler: null); + } + + public ulong Count() => ulong.CreateChecked(_links.Count(_any, _blogPost, _any)); + + public void Dispose() => (_links as IDisposable)?.Dispose(); +} diff --git a/csharp/SQLiteVSDoublets/Program.cs b/csharp/SQLiteVSDoublets/Program.cs new file mode 100644 index 0000000..2d022b2 --- /dev/null +++ b/csharp/SQLiteVSDoublets/Program.cs @@ -0,0 +1,64 @@ +// Usage: sqlite-vs-doublets <32|64> [--work N | --repetitions N] +// [--variants A,B] [--directory DIR] [--output FILE] +// +// Repetitions default to `work / size` clamped to 1..=MaxRepetitions, so smaller sizes get more, +// but not endless, repetitions. Objects get less work, because one blog post takes hundreds of links. + +using System.Text.Json; +using System.Text.Json.Nodes; +using Comparisons.SQLiteVSDoublets; +using Microsoft.Data.Sqlite; + +const ulong LinksWork = 3_000_000; +const ulong ObjectsWork = 500_000; +const ulong MaxRepetitions = 10; + +var (category, bits, sizeText) = (args[0], args[1], args[2]); +var options = args[3..].Chunk(2).ToDictionary(pair => pair[0].TrimStart('-'), pair => pair[1]); +static ulong Number(string text) => ulong.Parse(text.Replace("_", "")); +var size = Number(sizeText); +var defaultWork = category == "links" ? LinksWork : ObjectsWork; +var work = options.TryGetValue("work", out var workText) ? Number(workText) : defaultWork; +var repetitions = options.TryGetValue("repetitions", out var count) + ? int.Parse(count) + : (int)Math.Clamp(work / size, 1, MaxRepetitions); +var allVariants = category == "links" ? Harness.LinksVariants : Harness.ObjectsVariants; +var variants = options.TryGetValue("variants", out var names) + ? names.Split(',').Select(name => allVariants.Contains(name) ? name : throw new ArgumentException(name)).ToArray() + : allVariants; +var directory = options.GetValueOrDefault("directory") ?? Path.GetTempPath(); + +var measurements = variants.Select(variant => (category, bits) switch +{ + ("links", "32") => Harness.MeasureLinks(variant, size, repetitions, directory), + ("links", "64") => Harness.MeasureLinks(variant, size, repetitions, directory), + ("objects", "32") => Harness.MeasureObjects(variant, size, repetitions, directory), + ("objects", "64") => Harness.MeasureObjects(variant, size, repetitions, directory), + _ => throw new ArgumentException("category must be links or objects and bits must be 32 or 64"), +}).ToList(); + +using var connection = new SqliteConnection("Data Source=:memory:"); +connection.Open(); +using var version = connection.CreateCommand(); +version.CommandText = "SELECT sqlite_version()"; +var report = new JsonObject +{ + ["language"] = "C#", + ["category"] = category, + ["bits"] = int.Parse(bits), + ["size"] = size, + ["repetitions"] = repetitions, + ["warm_up_size"] = Math.Min(size, Harness.WarmUpSize), + ["warm_up_seconds"] = Harness.WarmUpTime.TotalSeconds, + ["sqlite_version"] = (string)version.ExecuteScalar()!, + ["results"] = new JsonArray(measurements.Select(measurement => (JsonNode)measurement.ToJson()).ToArray()), +}; +var json = report.ToJsonString(new JsonSerializerOptions { WriteIndented = true }); +if (options.TryGetValue("output", out var output)) +{ + File.WriteAllText(output, json); +} +else +{ + Console.WriteLine(json); +} diff --git a/csharp/SQLiteVSDoublets/SQLiteStorage.cs b/csharp/SQLiteVSDoublets/SQLiteStorage.cs new file mode 100644 index 0000000..05b4f20 --- /dev/null +++ b/csharp/SQLiteVSDoublets/SQLiteStorage.cs @@ -0,0 +1,66 @@ +using Microsoft.Data.Sqlite; +using SQLitePCL; + +namespace Comparisons.SQLiteVSDoublets; + +/// A connection with reusable prepared commands that all join the current transaction. +public abstract class SQLiteStorage : IDisposable +{ + private readonly SqliteConnection _connection; + private readonly List _commands = []; + private readonly string _table; + + protected SQLiteStorage(string path, string table, string schema) + { + _connection = new SqliteConnection($"Data Source={path};Pooling=False"); + _connection.Open(); + _table = table; + using var create = _connection.CreateCommand(); + create.CommandText = schema; + create.ExecuteNonQuery(); + } + + protected SqliteCommand Command(string sql, params (string Name, SqliteType Type)[] parameters) + { + var command = _connection.CreateCommand(); + command.CommandText = sql; + foreach (var (name, type) in parameters) + { + command.Parameters.Add(name, type); + } + _commands.Add(command); + return command; + } + + /// Like RETURNING id, but without its cost (3× slower inserts, see experiments/sqlite_returning). + protected long LastInsertRowId => raw.sqlite3_last_insert_rowid(_connection.Handle); + + public ulong Count() + { + using var count = _connection.CreateCommand(); + count.CommandText = $"SELECT COUNT(*) FROM {_table}"; + return (ulong)(long)count.ExecuteScalar()!; + } + + public TResult Transaction(Func work) + { + using var transaction = _connection.BeginTransaction(); + _commands.ForEach(command => command.Transaction = transaction); + try + { + var result = work(); + transaction.Commit(); + return result; + } + finally + { + _commands.ForEach(command => command.Transaction = null); + } + } + + public void Dispose() + { + _commands.ForEach(command => command.Dispose()); + _connection.Dispose(); + } +} diff --git a/csharp/SQLiteVSDoublets/SQLiteVSDoublets.csproj b/csharp/SQLiteVSDoublets/SQLiteVSDoublets.csproj new file mode 100644 index 0000000..fd447ae --- /dev/null +++ b/csharp/SQLiteVSDoublets/SQLiteVSDoublets.csproj @@ -0,0 +1,20 @@ + + + + Exe + net10.0 + enable + enable + Comparisons.SQLiteVSDoublets + sqlite-vs-doublets + true + true + + + + + + + + + diff --git a/csharp/SizeAfterCreationColumn.cs b/csharp/SizeAfterCreationColumn.cs deleted file mode 100644 index 4f107b8..0000000 --- a/csharp/SizeAfterCreationColumn.cs +++ /dev/null @@ -1,194 +0,0 @@ -using System; -using System.IO; -using System.Linq; -using BenchmarkDotNet.Columns; -using BenchmarkDotNet.Reports; -using BenchmarkDotNet.Running; - -namespace Comparisons.SQLiteVSDoublets -{ - /// - /// - /// Represents the size after creation column. - /// - /// - /// - /// - public class SizeAfterCreationColumn : IColumn - { - /// - /// - /// The sq lite vs doublets. - /// - /// - /// - public static readonly string DbSizeOutputFolder = Path.Combine(Path.GetTempPath(), nameof(Comparisons), nameof(SQLiteVSDoublets)); - - /// - /// - /// Gets the id value. - /// - /// - /// - public string Id => nameof(SizeAfterCreationColumn); - - /// - /// - /// Gets the column name value. - /// - /// - /// - public string ColumnName => "SizeAfterCreation"; - - /// - /// - /// Gets the legend value. - /// - /// - /// - public string Legend => "Allocated memory on disk after all records are created (1KB = 1024B)"; - - /// - /// - /// Gets the unit type value. - /// - /// - /// - public UnitType UnitType => UnitType.Size; - - /// - /// - /// Gets the always show value. - /// - /// - /// - public bool AlwaysShow => true; - - /// - /// - /// Gets the category value. - /// - /// - /// - public ColumnCategory Category => ColumnCategory.Metric; - - /// - /// - /// Gets the priority in category value. - /// - /// - /// - public int PriorityInCategory => 0; - - /// - /// - /// Gets the is numeric value. - /// - /// - /// - public bool IsNumeric => true; - - /// - /// - /// Determines whether this instance is available. - /// - /// - /// - /// - /// The summary. - /// - /// - /// - /// The bool - /// - /// - public bool IsAvailable(Summary summary) => true; - - /// - /// - /// Determines whether this instance is default. - /// - /// - /// - /// - /// The summary. - /// - /// - /// - /// The benchmark case. - /// - /// - /// - /// The bool - /// - /// - public bool IsDefault(Summary summary, BenchmarkCase benchmarkCase) => false; - - /// - /// - /// Gets the value using the specified summary. - /// - /// - /// - /// - /// The summary. - /// - /// - /// - /// The benchmark case. - /// - /// - /// - /// The string - /// - /// - public string GetValue(Summary summary, BenchmarkCase benchmarkCase) => GetValue(summary, benchmarkCase, SummaryStyle.Default); - - /// - /// - /// Gets the value using the specified summary. - /// - /// - /// - /// - /// The summary. - /// - /// - /// - /// The benchmark case. - /// - /// - /// - /// The style. - /// - /// - /// - /// The string - /// - /// - public string GetValue(Summary summary, BenchmarkCase benchmarkCase, SummaryStyle style) - { - var benchmarkName = benchmarkCase.Descriptor.WorkloadMethod.Name.ToLower(); - var parameter = benchmarkCase.Parameters.Items.FirstOrDefault(x => x.Name == "N"); - if (parameter == null) - { - return "no parameter"; - } - var N = Convert.ToInt32(parameter.Value); - var filename = Path.Combine(DbSizeOutputFolder, $"disk-size.{benchmarkName}.{N}.txt"); - return File.Exists(filename) ? File.ReadAllText(filename) : "no file"; - } - - /// - /// - /// Returns the string. - /// - /// - /// - /// - /// The string - /// - /// - public override string ToString() => ColumnName; - } -} diff --git a/csharp/TestRun.cs b/csharp/TestRun.cs deleted file mode 100644 index f022af6..0000000 --- a/csharp/TestRun.cs +++ /dev/null @@ -1,137 +0,0 @@ -using System.IO; -using System.Collections.Generic; -using Platform.IO; -using Platform.Diagnostics; -using Comparisons.SQLiteVSDoublets.Model; - -namespace Comparisons.SQLiteVSDoublets -{ - /// - /// - /// Represents the test run. - /// - /// - /// - public abstract class TestRun - { - /// - /// - /// Gets the db filename value. - /// - /// - /// - public string DbFilename { get; } - /// - /// - /// Gets the results value. - /// - /// - /// - public TestRunResults Results { get; } - /// - /// - /// Gets the read blog posts value. - /// - /// - /// - public List ReadBlogPosts { get; } - - /// - /// - /// Initializes a new instance. - /// - /// - /// - /// - /// A db filename. - /// - /// - protected TestRun(string dbFilename) - { - DbFilename = dbFilename; - Results = new TestRunResults(); - ReadBlogPosts = new List(); - } - - /// - /// - /// Runs this instance. - /// - /// - /// - public void Run() - { - if (File.Exists(DbFilename)) - { - DeleteDatabase(); - } - Results.PrepareTime = Performance.Measure(Prepare); - Results.DbSizeAfterPrepare = GetDatabaseSizeInBytes(); - Results.ListCreationTime = Performance.Measure(CreateList); - Results.DbSizeAfterCreation = GetDatabaseSizeInBytes(); - Results.ListReadingTime = Performance.Measure(ReadList); - Results.DbSizeAfterReading = GetDatabaseSizeInBytes(); - Results.ListDeletionTime = Performance.Measure(DeleteList); - Results.DbSizeAfterDeletion = GetDatabaseSizeInBytes(); - DeleteDatabase(); - } - - /// - /// - /// Gets the database size in bytes. - /// - /// - /// - /// - /// The long - /// - /// - protected virtual long GetDatabaseSizeInBytes() - { - return FileHelpers.GetSize(DbFilename); - } - - /// - /// - /// Deletes the database. - /// - /// - /// - protected virtual void DeleteDatabase() - { - File.Delete(DbFilename); - } - - /// - /// - /// Prepares this instance. - /// - /// - /// - public abstract void Prepare(); - - /// - /// - /// Creates the list. - /// - /// - /// - public abstract void CreateList(); - - /// - /// - /// Reads the list. - /// - /// - /// - public abstract void ReadList(); - - /// - /// - /// Deletes the list. - /// - /// - /// - public abstract void DeleteList(); - } -} diff --git a/csharp/TestRunResults.cs b/csharp/TestRunResults.cs deleted file mode 100644 index b802712..0000000 --- a/csharp/TestRunResults.cs +++ /dev/null @@ -1,119 +0,0 @@ -using System; -using System.Text; - -namespace Comparisons.SQLiteVSDoublets -{ - /// - /// - /// Represents the test run results. - /// - /// - /// - public class TestRunResults - { - /// - /// - /// Gets or sets the prepare time value. - /// - /// - /// - public TimeSpan PrepareTime { get; set; } - /// - /// - /// Gets or sets the db size after prepare value. - /// - /// - /// - public long DbSizeAfterPrepare { get; set; } - /// - /// - /// Gets or sets the list creation time value. - /// - /// - /// - public TimeSpan ListCreationTime { get; set; } - /// - /// - /// Gets or sets the db size after creation value. - /// - /// - /// - public long DbSizeAfterCreation { get; set; } - /// - /// - /// Gets or sets the list reading time value. - /// - /// - /// - public TimeSpan ListReadingTime { get; set; } - /// - /// - /// Gets or sets the db size after reading value. - /// - /// - /// - public long DbSizeAfterReading { get; set; } - /// - /// - /// Gets or sets the list deletion time value. - /// - /// - /// - public TimeSpan ListDeletionTime { get; set; } - /// - /// - /// Gets or sets the db size after deletion value. - /// - /// - /// - public long DbSizeAfterDeletion { get; set; } - - /// - /// - /// Returns the string. - /// - /// - /// - /// - /// The string - /// - /// - public override string ToString() - { - var sb = new StringBuilder(); - if (DbSizeAfterPrepare != 0) - { - sb.AppendLine($"Prepare execution time: {PrepareTime}, db size after prepare: {DbSizeAfterPrepare}."); - } - else - { - sb.AppendLine($"Prepare execution time: {PrepareTime}."); - } - if (DbSizeAfterCreation != 0) - { - sb.AppendLine($"Create list execution time: {ListCreationTime}, db size after list creation: {DbSizeAfterCreation}."); - } - else - { - sb.AppendLine($"Create list execution time: {ListCreationTime}."); - } - if (DbSizeAfterReading != 0) - { - sb.AppendLine($"Read list execution time: {ListReadingTime}, db size after list reading: {DbSizeAfterReading}."); - } - else - { - sb.AppendLine($"Read list execution time: {ListReadingTime}."); - } - if (DbSizeAfterDeletion != 0) - { - sb.AppendLine($"Delete list execution time: {ListDeletionTime}, db size after list deletion: {DbSizeAfterDeletion}."); - } - else - { - sb.AppendLine($"Delete list execution time: {ListDeletionTime}."); - } - return sb.ToString(); - } - } -} diff --git a/csharp/global.json b/csharp/global.json index 4c4c3ae..df32a2e 100644 --- a/csharp/global.json +++ b/csharp/global.json @@ -1,7 +1,10 @@ { "sdk": { - "version": "8.0.100", + "version": "10.0.100", "rollForward": "latestFeature", "allowPrerelease": false + }, + "test": { + "runner": "Microsoft.Testing.Platform" } } diff --git a/csharp/out.py b/csharp/out.py deleted file mode 100644 index 2a9eac4..0000000 --- a/csharp/out.py +++ /dev/null @@ -1,119 +0,0 @@ -#!/usr/bin/env python3 -"""Parse BenchmarkDotNet JSON and publish the C# link benchmark report.""" - -import argparse -import json -import os -import sys -from urllib.parse import parse_qs - -REPOSITORY_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -sys.path.insert(0, os.path.join(REPOSITORY_ROOT, "scripts")) - -import benchmark_report as report # noqa: E402 - - -METHODS = { - "Create": "create", - "Update": "update", - "Delete": "delete", - "EachAll": "query_all", - "EachIdentity": "query_by_id", - "EachConcrete": "query_by_source_target", - "EachOutgoing": "query_by_source", - "EachIncoming": "query_by_target", -} - - -def parse_document(document): - """Parse a BenchmarkDotNet full JSON export into shared report results.""" - results = report.empty_results(report.LINK_OPERATIONS) - for benchmark in document.get("Benchmarks", []): - operation = METHODS.get(benchmark.get("Method")) - parameters = parse_qs(benchmark.get("Parameters", "")) - variant = parameters.get("Variant", [None])[0] - mean = benchmark.get("Statistics", {}).get("Mean") - if operation in results and variant and isinstance(mean, (int, float)) and mean > 0: - results[operation][variant] = round(mean) - return results - - -def parse_results(path): - if not os.path.exists(path): - return report.empty_results(report.LINK_OPERATIONS) - with open(path, "r", encoding="utf-8") as handle: - return parse_document(json.load(handle)) - - -def parse_args(argv): - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("input", help="BenchmarkDotNet *-report-full.json file") - parser.add_argument("--results", default="results.md") - parser.add_argument("--readme", action="append", default=[]) - parser.add_argument("--docs-dir") - parser.add_argument("--output-dir", default="") - return parser.parse_args(argv) - - -def main(argv=None): - args = parse_args(argv if argv is not None else sys.argv[1:]) - try: - results = parse_results(args.input) - except (json.JSONDecodeError, OSError) as error: - print(f"Cannot parse {args.input}: {error}") - print(report.report_input_excerpt(args.input)) - return 1 - - if not report.has_any_results(results): - print(f"No benchmark data found in {args.input}") - print(report.report_input_excerpt(args.input)) - return 1 - - missing = report.missing_measurements( - results, - report.LINK_OPERATIONS, - report.VARIANTS, - ) - if missing: - print("Benchmark output is incomplete; missing:") - print("\n".join(missing)) - return 1 - - provenance = report.build_provenance(language="C#", object_count=False) - section = report.render_results_section( - results, - provenance, - operations=report.LINK_OPERATIONS, - ) - section += ( - "\n\n![C# benchmark comparison](docs/benchmarks/bench_csharp.png)" - "\n\n![C# benchmark comparison, logarithmic scale]" - "(docs/benchmarks/bench_csharp_log_scale.png)" - ) - with open(args.results, "w", encoding="utf-8") as handle: - handle.write(section + "\n") - print(report.format_results_table(results, operations=report.LINK_OPERATIONS)) - print(f"Generated {args.results}") - - charts = report.generate_charts( - results, - "bench_csharp", - "Benchmark Comparison: SQLite vs Doublets (C#)", - args.output_dir, - operations=report.LINK_OPERATIONS, - ) - report.copy_charts(charts, args.docs_dir) - - for readme in args.readme: - changed = report.update_markers( - readme, - section, - report.CSHARP_START_MARKER, - report.CSHARP_END_MARKER, - ) - print(f"{'Updated' if changed else 'No changes needed in'} {readme}") - return 0 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/csharp/results.md b/csharp/results.md deleted file mode 100644 index 93a6c7d..0000000 --- a/csharp/results.md +++ /dev/null @@ -1,16 +0,0 @@ -_Generated 2026-09-19 18:13 UTC for C# by [GitHub Actions run 35459522587](https://github.com/linksplatform/Comparisons.SQLiteVSDoublets/actions/runs/35459522587) — 1000 benchmarked links, 3000 background links._ - -| Operation | Doublets United Volatile | Doublets United NonVolatile | Doublets Split Volatile | Doublets Split NonVolatile | SQLite Memory | SQLite File | -|---------------|--------------------------|-----------------------------|-------------------------|----------------------------|---------------|-------------| -| Create | 2414327 (3.7x faster) | 2512520 (3.5x faster) | 867200 (10.2x faster) | 896999 (9.8x faster) | 8818051 | 2294476668 | -| Update | 4566053 (2.3x faster) | 4670899 (2.3x faster) | 1541871 (7.0x faster) | 1534119 (7.0x faster) | 10720700 | 2733072615 | -| Delete | 484008 (14.3x faster) | 465524 (14.8x faster) | 523910 (13.2x faster) | 521322 (13.3x faster) | 6907869 | 1682008343 | -| Each All | 437356 (3.7x faster) | 470885 (3.4x faster) | 420518 (3.8x faster) | 397666 (4.0x faster) | 2113072 | 1598861 | -| Each Identity | 430962 (11.6x faster) | 411173 (12.1x faster) | 403128 (12.4x faster) | 385531 (13.0x faster) | 4995738 | 12044311 | -| Each Concrete | 1363130 (5.2x faster) | 1408600 (5.1x faster) | 647769 (11.0x faster) | 917509 (7.8x faster) | 7134655 | 13622920 | -| Each Outgoing | 1160157 (5.3x faster) | 1180175 (5.3x faster) | 862790 (7.2x faster) | 830070 (7.5x faster) | 6199124 | 12923172 | -| Each Incoming | 1139559 (5.7x faster) | 1169325 (5.6x faster) | 796694 (8.2x faster) | 776158 (8.4x faster) | 6533836 | 13510164 | - -![C# benchmark comparison](docs/benchmarks/bench_csharp.png) - -![C# benchmark comparison, logarithmic scale](docs/benchmarks/bench_csharp_log_scale.png) diff --git a/csharp/test_out.py b/csharp/test_out.py deleted file mode 100644 index 0311234..0000000 --- a/csharp/test_out.py +++ /dev/null @@ -1,122 +0,0 @@ -#!/usr/bin/env python3 -"""Regression tests for the BenchmarkDotNet JSON report pipeline.""" - -import json -import os -import tempfile -import unittest - -import out - - -def complete_document(): - benchmarks = [] - mean = 1_000.4 - for method in out.METHODS: - for variant, _label, _color in out.report.VARIANTS: - benchmarks.append( - { - "Method": method, - "Parameters": f"N=10&Variant={variant}", - "Statistics": {"Mean": mean}, - } - ) - mean += 100 - return {"Title": "test", "Benchmarks": benchmarks} - - -class ParserTests(unittest.TestCase): - def test_parses_every_operation_and_variant(self): - results = out.parse_document(complete_document()) - - self.assertFalse( - out.report.missing_measurements( - results, - out.report.LINK_OPERATIONS, - out.report.VARIANTS, - ) - ) - self.assertEqual(results["create"]["Doublets_United_Volatile"], 1_000) - - def test_ignores_legacy_object_benchmarks(self): - document = complete_document() - document["Benchmarks"].append( - { - "Method": "SQLite", - "Parameters": "N=10", - "Statistics": {"Mean": 42}, - } - ) - - self.assertEqual(out.parse_document(document), out.parse_document(complete_document())) - - def test_ignores_failed_benchmarks_without_statistics(self): - results = out.parse_document( - { - "Benchmarks": [ - { - "Method": "Create", - "Parameters": "N=10&Variant=SQLite_Memory", - } - ] - } - ) - - self.assertFalse(out.report.has_any_results(results)) - - -class MainTests(unittest.TestCase): - def setUp(self): - self.temporary = tempfile.TemporaryDirectory() - self.addCleanup(self.temporary.cleanup) - self.input = os.path.join(self.temporary.name, "results.json") - self.results = os.path.join(self.temporary.name, "results.md") - self.readme = os.path.join(self.temporary.name, "README.md") - - def write_json(self, document): - with open(self.input, "w", encoding="utf-8") as handle: - json.dump(document, handle) - - def test_generates_results_and_updates_readme(self): - self.write_json(complete_document()) - with open(self.readme, "w", encoding="utf-8") as handle: - handle.write( - f"{out.report.CSHARP_START_MARKER}\nold\n" - f"{out.report.CSHARP_END_MARKER}\n" - ) - - result = out.main( - [ - self.input, - "--results", - self.results, - "--readme", - self.readme, - "--output-dir", - self.temporary.name, - ] - ) - - self.assertEqual(result, 0) - with open(self.results, "r", encoding="utf-8") as handle: - self.assertIn("Each Concrete", handle.read()) - with open(self.readme, "r", encoding="utf-8") as handle: - self.assertIn("SQLite File", handle.read()) - - def test_rejects_incomplete_results(self): - document = complete_document() - document["Benchmarks"].pop() - self.write_json(document) - - self.assertEqual(out.main([self.input, "--results", self.results]), 1) - self.assertFalse(os.path.exists(self.results)) - - def test_rejects_malformed_json(self): - with open(self.input, "w", encoding="utf-8") as handle: - handle.write("not json") - - self.assertEqual(out.main([self.input, "--results", self.results]), 1) - - -if __name__ == "__main__": - unittest.main() diff --git a/docs/benchmarks/bench_csharp.png b/docs/benchmarks/bench_csharp.png deleted file mode 100644 index 6e916d8..0000000 Binary files a/docs/benchmarks/bench_csharp.png and /dev/null differ diff --git a/docs/benchmarks/bench_csharp_log_scale.png b/docs/benchmarks/bench_csharp_log_scale.png deleted file mode 100644 index f5c59b7..0000000 Binary files a/docs/benchmarks/bench_csharp_log_scale.png and /dev/null differ diff --git a/docs/benchmarks/bench_rust.png b/docs/benchmarks/bench_rust.png deleted file mode 100644 index 7232ea2..0000000 Binary files a/docs/benchmarks/bench_rust.png and /dev/null differ diff --git a/docs/benchmarks/bench_rust_log_scale.png b/docs/benchmarks/bench_rust_log_scale.png deleted file mode 100644 index 8256088..0000000 Binary files a/docs/benchmarks/bench_rust_log_scale.png and /dev/null differ diff --git a/docs/benchmarks/links-csharp-32.png b/docs/benchmarks/links-csharp-32.png new file mode 100644 index 0000000..649d784 Binary files /dev/null and b/docs/benchmarks/links-csharp-32.png differ diff --git a/docs/benchmarks/links-csharp-64.png b/docs/benchmarks/links-csharp-64.png new file mode 100644 index 0000000..e17ed49 Binary files /dev/null and b/docs/benchmarks/links-csharp-64.png differ diff --git a/docs/benchmarks/links-rust-32.png b/docs/benchmarks/links-rust-32.png new file mode 100644 index 0000000..e5dad8c Binary files /dev/null and b/docs/benchmarks/links-rust-32.png differ diff --git a/docs/benchmarks/links-rust-64.png b/docs/benchmarks/links-rust-64.png new file mode 100644 index 0000000..56a71de Binary files /dev/null and b/docs/benchmarks/links-rust-64.png differ diff --git a/docs/benchmarks/objects-csharp-32.png b/docs/benchmarks/objects-csharp-32.png new file mode 100644 index 0000000..a995f24 Binary files /dev/null and b/docs/benchmarks/objects-csharp-32.png differ diff --git a/docs/benchmarks/objects-csharp-64.png b/docs/benchmarks/objects-csharp-64.png new file mode 100644 index 0000000..e76edd9 Binary files /dev/null and b/docs/benchmarks/objects-csharp-64.png differ diff --git a/docs/benchmarks/objects-rust-32.png b/docs/benchmarks/objects-rust-32.png new file mode 100644 index 0000000..d4ad689 Binary files /dev/null and b/docs/benchmarks/objects-rust-32.png differ diff --git a/docs/benchmarks/objects-rust-64.png b/docs/benchmarks/objects-rust-64.png new file mode 100644 index 0000000..2ad43e3 Binary files /dev/null and b/docs/benchmarks/objects-rust-64.png differ diff --git a/docs/benchmarks/results/links-csharp-32-100000.json b/docs/benchmarks/results/links-csharp-32-100000.json new file mode 100644 index 0000000..357c73f --- /dev/null +++ b/docs/benchmarks/results/links-csharp-32-100000.json @@ -0,0 +1,866 @@ +{ + "language": "C#", + "category": "links", + "bits": 32, + "size": 100000, + "repetitions": 10, + "warm_up_size": 10000, + "sqlite_version": "3.53.3", + "results": [ + { + "variant": "SQLite_Memory", + "file_bytes": null, + "operations": { + "create": { + "median_ns": 6272.5085, + "min_ns": 6203.279, + "max_ns": 7216.865, + "samples_ns": [ + 7216.865, + 6250.427, + 6324.591, + 6249.008, + 6274.833, + 6278.659, + 6298.092, + 6251.887, + 6203.279, + 6270.184 + ] + }, + "query_all": { + "median_ns": 380.0375, + "min_ns": 378.191, + "max_ns": 386.711, + "samples_ns": [ + 384.533, + 386.711, + 385.187, + 381.1, + 380.358, + 379.608, + 379.166, + 379.717, + 378.375, + 378.191 + ] + }, + "query_by_id": { + "median_ns": 1199.9505, + "min_ns": 1193.218, + "max_ns": 1208.367, + "samples_ns": [ + 1200.74, + 1205.254, + 1199.161, + 1195.77, + 1193.218, + 1195.586, + 1208.367, + 1202.902, + 1204.802, + 1198.743 + ] + }, + "query_by_from_to": { + "median_ns": 1456.6205, + "min_ns": 1451.53, + "max_ns": 1535.694, + "samples_ns": [ + 1535.694, + 1461.187, + 1482.475, + 1451.53, + 1452.823, + 1493.297, + 1455.791, + 1453.354, + 1456.827, + 1456.414 + ] + }, + "query_by_from": { + "median_ns": 1514.56, + "min_ns": 1499.252, + "max_ns": 1566.581, + "samples_ns": [ + 1566.581, + 1502.161, + 1514.337, + 1503.409, + 1499.252, + 1529.607, + 1514.783, + 1506.451, + 1535.553, + 1519.012 + ] + }, + "query_by_to": { + "median_ns": 1497.2035, + "min_ns": 1490.307, + "max_ns": 1660.609, + "samples_ns": [ + 1660.609, + 1490.307, + 1497.326, + 1497.081, + 1525.319, + 1493.409, + 1540.038, + 1500.699, + 1495.853, + 1494.539 + ] + }, + "update": { + "median_ns": 4272.395, + "min_ns": 4235.646, + "max_ns": 4624.422, + "samples_ns": [ + 4624.422, + 4280.756, + 4326.592, + 4352.997, + 4246.139, + 4264.556, + 4280.234, + 4241.626, + 4235.646, + 4240.328 + ] + }, + "delete": { + "median_ns": 2836.5245, + "min_ns": 2824.906, + "max_ns": 2911.91, + "samples_ns": [ + 2883.891, + 2911.91, + 2848.162, + 2824.906, + 2829.873, + 2835.613, + 2849.061, + 2826.178, + 2837.436, + 2829.656 + ] + } + } + }, + { + "variant": "SQLite_File", + "file_bytes": 4620288, + "operations": { + "create": { + "median_ns": 5748.014999999999, + "min_ns": 5715.064, + "max_ns": 5798.613, + "samples_ns": [ + 5762.864, + 5783.066, + 5798.613, + 5753.314, + 5745.571, + 5728.297, + 5732.955, + 5715.064, + 5750.459, + 5738.78 + ] + }, + "query_all": { + "median_ns": 384.6175, + "min_ns": 381.57, + "max_ns": 389.556, + "samples_ns": [ + 383.716, + 383.465, + 381.57, + 385.265, + 384.503, + 384.732, + 382.907, + 385.089, + 389.556, + 386.517 + ] + }, + "query_by_id": { + "median_ns": 1210.094, + "min_ns": 1203.093, + "max_ns": 1213.424, + "samples_ns": [ + 1203.626, + 1207.073, + 1210.156, + 1211.763, + 1210.553, + 1211.192, + 1203.093, + 1205.378, + 1210.032, + 1213.424 + ] + }, + "query_by_from_to": { + "median_ns": 1475.4865, + "min_ns": 1466.009, + "max_ns": 1487.966, + "samples_ns": [ + 1481.929, + 1477.914, + 1475.47, + 1469.147, + 1466.009, + 1478.941, + 1472.578, + 1469.337, + 1475.503, + 1487.966 + ] + }, + "query_by_from": { + "median_ns": 1524.647, + "min_ns": 1512.073, + "max_ns": 1533.378, + "samples_ns": [ + 1518.298, + 1528.891, + 1512.073, + 1524.437, + 1514.034, + 1527.359, + 1526.157, + 1518.274, + 1524.857, + 1533.378 + ] + }, + "query_by_to": { + "median_ns": 1519.5925, + "min_ns": 1509.422, + "max_ns": 1532.065, + "samples_ns": [ + 1516.425, + 1521.028, + 1518.677, + 1520.508, + 1512.862, + 1511.831, + 1525.813, + 1509.422, + 1522.49, + 1532.065 + ] + }, + "update": { + "median_ns": 12152.626, + "min_ns": 12021.147, + "max_ns": 12373.984, + "samples_ns": [ + 12294.263, + 12373.984, + 12244.843, + 12118.34, + 12021.147, + 12026.04, + 12226.674, + 12082.969, + 12095.999, + 12186.912 + ] + }, + "delete": { + "median_ns": 6224.0779999999995, + "min_ns": 6170.438, + "max_ns": 6363.415, + "samples_ns": [ + 6282.883, + 6277.358, + 6363.415, + 6223.841, + 6224.315, + 6180.04, + 6248.515, + 6181.058, + 6176.19, + 6170.438 + ] + } + } + }, + { + "variant": "Doublets_United_Volatile", + "file_bytes": null, + "operations": { + "create": { + "median_ns": 1024.179, + "min_ns": 1010.172, + "max_ns": 2700.137, + "samples_ns": [ + 2700.137, + 1027.06, + 1021.298, + 1049.064, + 1014.607, + 1020.162, + 1010.172, + 1119.473, + 1021.028, + 1112.066 + ] + }, + "query_all": { + "median_ns": 14.8985, + "min_ns": 14.505, + "max_ns": 139.96, + "samples_ns": [ + 139.96, + 14.505, + 14.862, + 15.2, + 15.016, + 14.692, + 15.224, + 14.628, + 14.935, + 14.826 + ] + }, + "query_by_id": { + "median_ns": 60.1185, + "min_ns": 56.46, + "max_ns": 344.262, + "samples_ns": [ + 344.262, + 57.268, + 57.186, + 61.925, + 58.704, + 59.877, + 56.46, + 60.36, + 151.572, + 60.654 + ] + }, + "query_by_from_to": { + "median_ns": 225.7975, + "min_ns": 217.595, + "max_ns": 406.194, + "samples_ns": [ + 301.112, + 219.577, + 218.88, + 230.795, + 220.8, + 218.605, + 217.595, + 231.237, + 406.194, + 240.698 + ] + }, + "query_by_from": { + "median_ns": 350.3075, + "min_ns": 312.518, + "max_ns": 469.064, + "samples_ns": [ + 312.518, + 360.75, + 349.448, + 354.208, + 351.167, + 349.169, + 347.874, + 363.633, + 469.064, + 348.072 + ] + }, + "query_by_to": { + "median_ns": 264.24, + "min_ns": 259.883, + "max_ns": 391.964, + "samples_ns": [ + 321.458, + 263.483, + 262.296, + 265.31, + 264.921, + 264.343, + 262.752, + 264.137, + 391.964, + 259.883 + ] + }, + "update": { + "median_ns": 2034.904, + "min_ns": 2021.661, + "max_ns": 2121.997, + "samples_ns": [ + 2021.661, + 2032.577, + 2026.753, + 2037.081, + 2048.025, + 2039.195, + 2029.769, + 2035.757, + 2121.997, + 2034.051 + ] + }, + "delete": { + "median_ns": 1050.5614999999998, + "min_ns": 1024.907, + "max_ns": 1331.536, + "samples_ns": [ + 1331.536, + 1048.861, + 1052.835, + 1053.613, + 1039.111, + 1050.051, + 1042.759, + 1051.072, + 1134.155, + 1024.907 + ] + } + } + }, + { + "variant": "Doublets_United_NonVolatile", + "file_bytes": 33554432, + "operations": { + "create": { + "median_ns": 1080.298, + "min_ns": 1062.952, + "max_ns": 1147.632, + "samples_ns": [ + 1079.435, + 1066.566, + 1062.952, + 1147.632, + 1081.161, + 1091.101, + 1142.446, + 1071.948, + 1083.141, + 1077.283 + ] + }, + "query_all": { + "median_ns": 14.8205, + "min_ns": 14.57, + "max_ns": 15.425, + "samples_ns": [ + 14.779, + 15.425, + 14.706, + 14.827, + 14.57, + 14.744, + 15.168, + 14.96, + 15.021, + 14.814 + ] + }, + "query_by_id": { + "median_ns": 60.4585, + "min_ns": 57.984, + "max_ns": 64.668, + "samples_ns": [ + 60.1, + 64.668, + 57.984, + 58.998, + 63.748, + 60.817, + 61.731, + 62.285, + 59.929, + 59.527 + ] + }, + "query_by_from_to": { + "median_ns": 257.71849999999995, + "min_ns": 247.684, + "max_ns": 263.242, + "samples_ns": [ + 256.61, + 261.072, + 247.684, + 257.049, + 256.263, + 260.728, + 260.532, + 258.388, + 263.242, + 256.542 + ] + }, + "query_by_from": { + "median_ns": 365.5375, + "min_ns": 354.268, + "max_ns": 367.678, + "samples_ns": [ + 358.506, + 356.457, + 354.268, + 366.772, + 365.401, + 366.08, + 367.678, + 365.674, + 366.91, + 363.645 + ] + }, + "query_by_to": { + "median_ns": 267.37350000000004, + "min_ns": 261.593, + "max_ns": 274.431, + "samples_ns": [ + 262.366, + 261.593, + 266.242, + 274.431, + 269.451, + 273.986, + 270.615, + 267.101, + 267.646, + 264.809 + ] + }, + "update": { + "median_ns": 2108.982, + "min_ns": 2086.756, + "max_ns": 2212.154, + "samples_ns": [ + 2086.835, + 2109.249, + 2119.218, + 2111.419, + 2086.756, + 2212.154, + 2192.985, + 2108.715, + 2094.089, + 2091.899 + ] + }, + "delete": { + "median_ns": 1057.281, + "min_ns": 1042.721, + "max_ns": 1110.043, + "samples_ns": [ + 1110.043, + 1042.721, + 1076.264, + 1058.2, + 1056.362, + 1058.91, + 1049.602, + 1062.227, + 1047.277, + 1049.546 + ] + } + } + }, + { + "variant": "Doublets_Split_Volatile", + "file_bytes": null, + "operations": { + "create": { + "median_ns": 191.9205, + "min_ns": 189.167, + "max_ns": 608.122, + "samples_ns": [ + 608.122, + 222.273, + 351.092, + 191.532, + 192.389, + 190.027, + 189.36, + 189.167, + 191.722, + 192.119 + ] + }, + "query_all": { + "median_ns": 16.034, + "min_ns": 15.755, + "max_ns": 17.149, + "samples_ns": [ + 17.149, + 16.894, + 16.586, + 15.942, + 15.958, + 15.755, + 15.987, + 16.363, + 16.081, + 15.899 + ] + }, + "query_by_id": { + "median_ns": 69.6215, + "min_ns": 67.891, + "max_ns": 254.43, + "samples_ns": [ + 254.43, + 71.229, + 69.801, + 73.757, + 68.156, + 67.891, + 70.281, + 68.518, + 68.635, + 69.442 + ] + }, + "query_by_from_to": { + "median_ns": 106.52850000000001, + "min_ns": 103.589, + "max_ns": 357.32, + "samples_ns": [ + 357.32, + 113.808, + 107.318, + 104.764, + 104.651, + 107.735, + 107.758, + 103.589, + 105.739, + 104.902 + ] + }, + "query_by_from": { + "median_ns": 84.497, + "min_ns": 82.199, + "max_ns": 209.142, + "samples_ns": [ + 209.142, + 86.678, + 88.383, + 84.17, + 82.8, + 86.332, + 82.964, + 84.824, + 83.193, + 82.199 + ] + }, + "query_by_to": { + "median_ns": 84.7755, + "min_ns": 82.62, + "max_ns": 106.984, + "samples_ns": [ + 106.984, + 82.62, + 85.104, + 84.218, + 84.003, + 85.065, + 85.377, + 85.028, + 84.523, + 83.743 + ] + }, + "update": { + "median_ns": 218.0575, + "min_ns": 216.275, + "max_ns": 718.916, + "samples_ns": [ + 718.916, + 220.193, + 220.592, + 217.985, + 216.275, + 217.006, + 219.118, + 217.537, + 216.598, + 218.13 + ] + }, + "delete": { + "median_ns": 276.1325, + "min_ns": 270.968, + "max_ns": 402.232, + "samples_ns": [ + 402.232, + 280.453, + 294.244, + 277.388, + 270.968, + 280.729, + 273.402, + 274.877, + 272.637, + 272.299 + ] + } + } + }, + { + "variant": "Doublets_Split_NonVolatile", + "file_bytes": 41943040, + "operations": { + "create": { + "median_ns": 240.5765, + "min_ns": 231.557, + "max_ns": 345.678, + "samples_ns": [ + 234.873, + 234.165, + 256.513, + 231.557, + 240.229, + 232.205, + 345.678, + 242.307, + 247.024, + 240.924 + ] + }, + "query_all": { + "median_ns": 16.466, + "min_ns": 15.965, + "max_ns": 17.302, + "samples_ns": [ + 16.509, + 16.356, + 17.302, + 16.066, + 16.719, + 15.965, + 16.879, + 16.262, + 16.909, + 16.423 + ] + }, + "query_by_id": { + "median_ns": 69.1155, + "min_ns": 67.336, + "max_ns": 72.419, + "samples_ns": [ + 69.734, + 68.712, + 72.419, + 68.595, + 68.954, + 68.68, + 72.158, + 69.277, + 69.552, + 67.336 + ] + }, + "query_by_from_to": { + "median_ns": 109.686, + "min_ns": 106.496, + "max_ns": 116.472, + "samples_ns": [ + 113.173, + 111.033, + 116.472, + 106.703, + 106.824, + 108.339, + 111.179, + 106.652, + 112.843, + 106.496 + ] + }, + "query_by_from": { + "median_ns": 86.6765, + "min_ns": 83.035, + "max_ns": 98.895, + "samples_ns": [ + 91.859, + 98.895, + 88.748, + 83.035, + 83.981, + 83.353, + 92.869, + 86.708, + 86.645, + 83.189 + ] + }, + "query_by_to": { + "median_ns": 86.4245, + "min_ns": 83.949, + "max_ns": 88.964, + "samples_ns": [ + 86.252, + 88.75, + 85.823, + 83.949, + 88.964, + 84.189, + 88.709, + 86.597, + 87.874, + 84.464 + ] + }, + "update": { + "median_ns": 224.63049999999998, + "min_ns": 222.38, + "max_ns": 228.397, + "samples_ns": [ + 224.447, + 226.806, + 226.046, + 222.51, + 222.544, + 222.38, + 226.026, + 223.258, + 228.397, + 224.814 + ] + }, + "delete": { + "median_ns": 263.402, + "min_ns": 251.907, + "max_ns": 315.564, + "samples_ns": [ + 263.962, + 315.564, + 255.312, + 251.907, + 254.523, + 258.586, + 265.898, + 262.842, + 266.836, + 266.254 + ] + } + } + } + ], + "run_url": "https://github.com/linksplatform/Comparisons.SQLiteVSDoublets/actions/runs/37226110636", + "date": "2026-10-04", + "machine": "Ubuntu 24.04.5 LTS, AMD EPYC 7763 64-Core Processor, 4 cores, 16 GiB" +} diff --git a/docs/benchmarks/results/links-csharp-32-1000000.json b/docs/benchmarks/results/links-csharp-32-1000000.json new file mode 100644 index 0000000..d648363 --- /dev/null +++ b/docs/benchmarks/results/links-csharp-32-1000000.json @@ -0,0 +1,530 @@ +{ + "language": "C#", + "category": "links", + "bits": 32, + "size": 1000000, + "repetitions": 3, + "warm_up_size": 10000, + "sqlite_version": "3.53.3", + "results": [ + { + "variant": "SQLite_Memory", + "file_bytes": null, + "operations": { + "create": { + "median_ns": 7045.3265, + "min_ns": 5860.6439, + "max_ns": 7108.3145, + "samples_ns": [ + 7108.3145, + 7045.3265, + 5860.6439 + ] + }, + "query_all": { + "median_ns": 428.4999, + "min_ns": 375.5231, + "max_ns": 430.3737, + "samples_ns": [ + 428.4999, + 430.3737, + 375.5231 + ] + }, + "query_by_id": { + "median_ns": 1510.5098, + "min_ns": 1263.6444, + "max_ns": 1531.8232, + "samples_ns": [ + 1531.8232, + 1510.5098, + 1263.6444 + ] + }, + "query_by_from_to": { + "median_ns": 1689.0197, + "min_ns": 1444.8741, + "max_ns": 1727.9844, + "samples_ns": [ + 1689.0197, + 1727.9844, + 1444.8741 + ] + }, + "query_by_from": { + "median_ns": 1763.7771, + "min_ns": 1518.3727, + "max_ns": 1776.0572, + "samples_ns": [ + 1763.7771, + 1776.0572, + 1518.3727 + ] + }, + "query_by_to": { + "median_ns": 1769.0797, + "min_ns": 1482.3885, + "max_ns": 1782.0364, + "samples_ns": [ + 1782.0364, + 1769.0797, + 1482.3885 + ] + }, + "update": { + "median_ns": 6761.1146, + "min_ns": 5855.3279, + "max_ns": 7332.8588, + "samples_ns": [ + 6761.1146, + 7332.8588, + 5855.3279 + ] + }, + "delete": { + "median_ns": 4116.9166, + "min_ns": 3842.6377, + "max_ns": 4466.6686, + "samples_ns": [ + 4466.6686, + 4116.9166, + 3842.6377 + ] + } + } + }, + { + "variant": "SQLite_File", + "file_bytes": 50540544, + "operations": { + "create": { + "median_ns": 6890.4023, + "min_ns": 6869.107, + "max_ns": 7811.4438, + "samples_ns": [ + 7811.4438, + 6869.107, + 6890.4023 + ] + }, + "query_all": { + "median_ns": 385.382, + "min_ns": 385.134, + "max_ns": 421.4624, + "samples_ns": [ + 385.134, + 385.382, + 421.4624 + ] + }, + "query_by_id": { + "median_ns": 1805.1359, + "min_ns": 1805.0347, + "max_ns": 2021.5445, + "samples_ns": [ + 1805.0347, + 1805.1359, + 2021.5445 + ] + }, + "query_by_from_to": { + "median_ns": 1946.6014, + "min_ns": 1915.9391, + "max_ns": 2222.9758, + "samples_ns": [ + 1915.9391, + 1946.6014, + 2222.9758 + ] + }, + "query_by_from": { + "median_ns": 2023.5183, + "min_ns": 2019.9763, + "max_ns": 2407.6168, + "samples_ns": [ + 2019.9763, + 2023.5183, + 2407.6168 + ] + }, + "query_by_to": { + "median_ns": 2037.14, + "min_ns": 2018.4121, + "max_ns": 2386.9402, + "samples_ns": [ + 2018.4121, + 2037.14, + 2386.9402 + ] + }, + "update": { + "median_ns": 12287.2821, + "min_ns": 12127.3118, + "max_ns": 14131.9384, + "samples_ns": [ + 12127.3118, + 12287.2821, + 14131.9384 + ] + }, + "delete": { + "median_ns": 8070.0253, + "min_ns": 8039.8917, + "max_ns": 8167.8735, + "samples_ns": [ + 8039.8917, + 8167.8735, + 8070.0253 + ] + } + } + }, + { + "variant": "Doublets_United_Volatile", + "file_bytes": null, + "operations": { + "create": { + "median_ns": 1695.2957, + "min_ns": 1645.7099, + "max_ns": 1860.7466, + "samples_ns": [ + 1860.7466, + 1645.7099, + 1695.2957 + ] + }, + "query_all": { + "median_ns": 16.4232, + "min_ns": 16.1103, + "max_ns": 27.0435, + "samples_ns": [ + 27.0435, + 16.1103, + 16.4232 + ] + }, + "query_by_id": { + "median_ns": 116.8544, + "min_ns": 108.3582, + "max_ns": 128.0984, + "samples_ns": [ + 128.0984, + 116.8544, + 108.3582 + ] + }, + "query_by_from_to": { + "median_ns": 573.6724, + "min_ns": 544.383, + "max_ns": 577.037, + "samples_ns": [ + 577.037, + 544.383, + 573.6724 + ] + }, + "query_by_from": { + "median_ns": 691.369, + "min_ns": 665.2816, + "max_ns": 746.1082, + "samples_ns": [ + 691.369, + 746.1082, + 665.2816 + ] + }, + "query_by_to": { + "median_ns": 824.4072, + "min_ns": 804.8646, + "max_ns": 956.5475, + "samples_ns": [ + 956.5475, + 824.4072, + 804.8646 + ] + }, + "update": { + "median_ns": 4297.6547, + "min_ns": 4040.7647, + "max_ns": 4704.1709, + "samples_ns": [ + 4704.1709, + 4297.6547, + 4040.7647 + ] + }, + "delete": { + "median_ns": 1848.5221, + "min_ns": 1839.511, + "max_ns": 1887.8209, + "samples_ns": [ + 1887.8209, + 1839.511, + 1848.5221 + ] + } + } + }, + { + "variant": "Doublets_United_NonVolatile", + "file_bytes": 33554432, + "operations": { + "create": { + "median_ns": 1854.5082, + "min_ns": 1851.6284, + "max_ns": 1994.9519, + "samples_ns": [ + 1851.6284, + 1854.5082, + 1994.9519 + ] + }, + "query_all": { + "median_ns": 16.4592, + "min_ns": 15.646, + "max_ns": 17.699, + "samples_ns": [ + 15.646, + 16.4592, + 17.699 + ] + }, + "query_by_id": { + "median_ns": 123.0902, + "min_ns": 108.1354, + "max_ns": 140.0126, + "samples_ns": [ + 123.0902, + 108.1354, + 140.0126 + ] + }, + "query_by_from_to": { + "median_ns": 688.2349, + "min_ns": 670.7453, + "max_ns": 801.4944, + "samples_ns": [ + 688.2349, + 670.7453, + 801.4944 + ] + }, + "query_by_from": { + "median_ns": 861.0351, + "min_ns": 789.8503, + "max_ns": 996.2587, + "samples_ns": [ + 861.0351, + 789.8503, + 996.2587 + ] + }, + "query_by_to": { + "median_ns": 981.7806, + "min_ns": 934.3808, + "max_ns": 1098.6292, + "samples_ns": [ + 934.3808, + 981.7806, + 1098.6292 + ] + }, + "update": { + "median_ns": 4932.576, + "min_ns": 4808.6799, + "max_ns": 5989.4189, + "samples_ns": [ + 4808.6799, + 4932.576, + 5989.4189 + ] + }, + "delete": { + "median_ns": 2249.2118, + "min_ns": 2198.2746, + "max_ns": 2552.2232, + "samples_ns": [ + 2198.2746, + 2249.2118, + 2552.2232 + ] + } + } + }, + { + "variant": "Doublets_Split_Volatile", + "file_bytes": null, + "operations": { + "create": { + "median_ns": 399.523, + "min_ns": 398.3911, + "max_ns": 489.5329, + "samples_ns": [ + 489.5329, + 399.523, + 398.3911 + ] + }, + "query_all": { + "median_ns": 16.7924, + "min_ns": 15.5431, + "max_ns": 16.8968, + "samples_ns": [ + 15.5431, + 16.8968, + 16.7924 + ] + }, + "query_by_id": { + "median_ns": 230.8884, + "min_ns": 228.4343, + "max_ns": 249.7326, + "samples_ns": [ + 249.7326, + 228.4343, + 230.8884 + ] + }, + "query_by_from_to": { + "median_ns": 396.1921, + "min_ns": 393.7545, + "max_ns": 410.6207, + "samples_ns": [ + 393.7545, + 410.6207, + 396.1921 + ] + }, + "query_by_from": { + "median_ns": 324.463, + "min_ns": 313.4045, + "max_ns": 326.4379, + "samples_ns": [ + 313.4045, + 326.4379, + 324.463 + ] + }, + "query_by_to": { + "median_ns": 337.5447, + "min_ns": 331.0825, + "max_ns": 339.5594, + "samples_ns": [ + 339.5594, + 331.0825, + 337.5447 + ] + }, + "update": { + "median_ns": 593.7112, + "min_ns": 530.5904, + "max_ns": 656.7094, + "samples_ns": [ + 530.5904, + 593.7112, + 656.7094 + ] + }, + "delete": { + "median_ns": 674.153, + "min_ns": 616.0927, + "max_ns": 681.8083, + "samples_ns": [ + 616.0927, + 674.153, + 681.8083 + ] + } + } + }, + { + "variant": "Doublets_Split_NonVolatile", + "file_bytes": 41943040, + "operations": { + "create": { + "median_ns": 466.733, + "min_ns": 445.8104, + "max_ns": 472.1553, + "samples_ns": [ + 472.1553, + 466.733, + 445.8104 + ] + }, + "query_all": { + "median_ns": 17.5994, + "min_ns": 17.3545, + "max_ns": 17.7383, + "samples_ns": [ + 17.3545, + 17.5994, + 17.7383 + ] + }, + "query_by_id": { + "median_ns": 237.9995, + "min_ns": 228.2973, + "max_ns": 262.17, + "samples_ns": [ + 262.17, + 228.2973, + 237.9995 + ] + }, + "query_by_from_to": { + "median_ns": 439.1206, + "min_ns": 368.7786, + "max_ns": 449.9522, + "samples_ns": [ + 449.9522, + 439.1206, + 368.7786 + ] + }, + "query_by_from": { + "median_ns": 368.7641, + "min_ns": 320.3801, + "max_ns": 371.4625, + "samples_ns": [ + 371.4625, + 368.7641, + 320.3801 + ] + }, + "query_by_to": { + "median_ns": 357.1689, + "min_ns": 323.8649, + "max_ns": 366.2974, + "samples_ns": [ + 366.2974, + 357.1689, + 323.8649 + ] + }, + "update": { + "median_ns": 618.4261, + "min_ns": 530.6773, + "max_ns": 687.1405, + "samples_ns": [ + 687.1405, + 618.4261, + 530.6773 + ] + }, + "delete": { + "median_ns": 600.9061, + "min_ns": 567.0869, + "max_ns": 712.6025, + "samples_ns": [ + 712.6025, + 600.9061, + 567.0869 + ] + } + } + } + ], + "run_url": "https://github.com/linksplatform/Comparisons.SQLiteVSDoublets/actions/runs/37226110636", + "date": "2026-10-04", + "machine": "Ubuntu 24.04.5 LTS, Intel(R) Xeon(R) 6973P-C, 4 cores, 16 GiB" +} diff --git a/docs/benchmarks/results/links-csharp-32-10000000.json b/docs/benchmarks/results/links-csharp-32-10000000.json new file mode 100644 index 0000000..d2d2f6d --- /dev/null +++ b/docs/benchmarks/results/links-csharp-32-10000000.json @@ -0,0 +1,434 @@ +{ + "language": "C#", + "category": "links", + "bits": 32, + "size": 10000000, + "repetitions": 1, + "warm_up_size": 10000, + "sqlite_version": "3.53.3", + "results": [ + { + "variant": "SQLite_Memory", + "file_bytes": null, + "operations": { + "create": { + "median_ns": 7409.439, + "min_ns": 7409.439, + "max_ns": 7409.439, + "samples_ns": [ + 7409.439 + ] + }, + "query_all": { + "median_ns": 316.35596, + "min_ns": 316.35596, + "max_ns": 316.35596, + "samples_ns": [ + 316.35596 + ] + }, + "query_by_id": { + "median_ns": 1855.09548, + "min_ns": 1855.09548, + "max_ns": 1855.09548, + "samples_ns": [ + 1855.09548 + ] + }, + "query_by_from_to": { + "median_ns": 2009.43124, + "min_ns": 2009.43124, + "max_ns": 2009.43124, + "samples_ns": [ + 2009.43124 + ] + }, + "query_by_from": { + "median_ns": 2059.61092, + "min_ns": 2059.61092, + "max_ns": 2059.61092, + "samples_ns": [ + 2059.61092 + ] + }, + "query_by_to": { + "median_ns": 2064.52439, + "min_ns": 2064.52439, + "max_ns": 2064.52439, + "samples_ns": [ + 2064.52439 + ] + }, + "update": { + "median_ns": 9494.77645, + "min_ns": 9494.77645, + "max_ns": 9494.77645, + "samples_ns": [ + 9494.77645 + ] + }, + "delete": { + "median_ns": 6488.99373, + "min_ns": 6488.99373, + "max_ns": 6488.99373, + "samples_ns": [ + 6488.99373 + ] + } + } + }, + { + "variant": "SQLite_File", + "file_bytes": 525307904, + "operations": { + "create": { + "median_ns": 18197.14077, + "min_ns": 18197.14077, + "max_ns": 18197.14077, + "samples_ns": [ + 18197.14077 + ] + }, + "query_all": { + "median_ns": 318.40655, + "min_ns": 318.40655, + "max_ns": 318.40655, + "samples_ns": [ + 318.40655 + ] + }, + "query_by_id": { + "median_ns": 2721.08251, + "min_ns": 2721.08251, + "max_ns": 2721.08251, + "samples_ns": [ + 2721.08251 + ] + }, + "query_by_from_to": { + "median_ns": 3156.89073, + "min_ns": 3156.89073, + "max_ns": 3156.89073, + "samples_ns": [ + 3156.89073 + ] + }, + "query_by_from": { + "median_ns": 3110.84609, + "min_ns": 3110.84609, + "max_ns": 3110.84609, + "samples_ns": [ + 3110.84609 + ] + }, + "query_by_to": { + "median_ns": 3099.94163, + "min_ns": 3099.94163, + "max_ns": 3099.94163, + "samples_ns": [ + 3099.94163 + ] + }, + "update": { + "median_ns": 24645.40124, + "min_ns": 24645.40124, + "max_ns": 24645.40124, + "samples_ns": [ + 24645.40124 + ] + }, + "delete": { + "median_ns": 14770.49596, + "min_ns": 14770.49596, + "max_ns": 14770.49596, + "samples_ns": [ + 14770.49596 + ] + } + } + }, + { + "variant": "Doublets_United_Volatile", + "file_bytes": null, + "operations": { + "create": { + "median_ns": 3013.31223, + "min_ns": 3013.31223, + "max_ns": 3013.31223, + "samples_ns": [ + 3013.31223 + ] + }, + "query_all": { + "median_ns": 10.0237, + "min_ns": 10.0237, + "max_ns": 10.0237, + "samples_ns": [ + 10.0237 + ] + }, + "query_by_id": { + "median_ns": 180.80623, + "min_ns": 180.80623, + "max_ns": 180.80623, + "samples_ns": [ + 180.80623 + ] + }, + "query_by_from_to": { + "median_ns": 1445.55734, + "min_ns": 1445.55734, + "max_ns": 1445.55734, + "samples_ns": [ + 1445.55734 + ] + }, + "query_by_from": { + "median_ns": 1669.18114, + "min_ns": 1669.18114, + "max_ns": 1669.18114, + "samples_ns": [ + 1669.18114 + ] + }, + "query_by_to": { + "median_ns": 1881.08252, + "min_ns": 1881.08252, + "max_ns": 1881.08252, + "samples_ns": [ + 1881.08252 + ] + }, + "update": { + "median_ns": 8158.94646, + "min_ns": 8158.94646, + "max_ns": 8158.94646, + "samples_ns": [ + 8158.94646 + ] + }, + "delete": { + "median_ns": 3515.51032, + "min_ns": 3515.51032, + "max_ns": 3515.51032, + "samples_ns": [ + 3515.51032 + ] + } + } + }, + { + "variant": "Doublets_United_NonVolatile", + "file_bytes": 335544320, + "operations": { + "create": { + "median_ns": 3949.20813, + "min_ns": 3949.20813, + "max_ns": 3949.20813, + "samples_ns": [ + 3949.20813 + ] + }, + "query_all": { + "median_ns": 14.06911, + "min_ns": 14.06911, + "max_ns": 14.06911, + "samples_ns": [ + 14.06911 + ] + }, + "query_by_id": { + "median_ns": 187.23678, + "min_ns": 187.23678, + "max_ns": 187.23678, + "samples_ns": [ + 187.23678 + ] + }, + "query_by_from_to": { + "median_ns": 1443.08329, + "min_ns": 1443.08329, + "max_ns": 1443.08329, + "samples_ns": [ + 1443.08329 + ] + }, + "query_by_from": { + "median_ns": 1636.64527, + "min_ns": 1636.64527, + "max_ns": 1636.64527, + "samples_ns": [ + 1636.64527 + ] + }, + "query_by_to": { + "median_ns": 1927.57033, + "min_ns": 1927.57033, + "max_ns": 1927.57033, + "samples_ns": [ + 1927.57033 + ] + }, + "update": { + "median_ns": 8988.75876, + "min_ns": 8988.75876, + "max_ns": 8988.75876, + "samples_ns": [ + 8988.75876 + ] + }, + "delete": { + "median_ns": 4351.46438, + "min_ns": 4351.46438, + "max_ns": 4351.46438, + "samples_ns": [ + 4351.46438 + ] + } + } + }, + { + "variant": "Doublets_Split_Volatile", + "file_bytes": null, + "operations": { + "create": { + "median_ns": 430.51851, + "min_ns": 430.51851, + "max_ns": 430.51851, + "samples_ns": [ + 430.51851 + ] + }, + "query_all": { + "median_ns": 10.27517, + "min_ns": 10.27517, + "max_ns": 10.27517, + "samples_ns": [ + 10.27517 + ] + }, + "query_by_id": { + "median_ns": 199.38015, + "min_ns": 199.38015, + "max_ns": 199.38015, + "samples_ns": [ + 199.38015 + ] + }, + "query_by_from_to": { + "median_ns": 474.96256, + "min_ns": 474.96256, + "max_ns": 474.96256, + "samples_ns": [ + 474.96256 + ] + }, + "query_by_from": { + "median_ns": 322.71037, + "min_ns": 322.71037, + "max_ns": 322.71037, + "samples_ns": [ + 322.71037 + ] + }, + "query_by_to": { + "median_ns": 324.11629, + "min_ns": 324.11629, + "max_ns": 324.11629, + "samples_ns": [ + 324.11629 + ] + }, + "update": { + "median_ns": 795.21355, + "min_ns": 795.21355, + "max_ns": 795.21355, + "samples_ns": [ + 795.21355 + ] + }, + "delete": { + "median_ns": 573.79597, + "min_ns": 573.79597, + "max_ns": 573.79597, + "samples_ns": [ + 573.79597 + ] + } + } + }, + { + "variant": "Doublets_Split_NonVolatile", + "file_bytes": 419430400, + "operations": { + "create": { + "median_ns": 1155.54854, + "min_ns": 1155.54854, + "max_ns": 1155.54854, + "samples_ns": [ + 1155.54854 + ] + }, + "query_all": { + "median_ns": 9.10093, + "min_ns": 9.10093, + "max_ns": 9.10093, + "samples_ns": [ + 9.10093 + ] + }, + "query_by_id": { + "median_ns": 210.61277, + "min_ns": 210.61277, + "max_ns": 210.61277, + "samples_ns": [ + 210.61277 + ] + }, + "query_by_from_to": { + "median_ns": 508.48379, + "min_ns": 508.48379, + "max_ns": 508.48379, + "samples_ns": [ + 508.48379 + ] + }, + "query_by_from": { + "median_ns": 353.94354, + "min_ns": 353.94354, + "max_ns": 353.94354, + "samples_ns": [ + 353.94354 + ] + }, + "query_by_to": { + "median_ns": 356.72441, + "min_ns": 356.72441, + "max_ns": 356.72441, + "samples_ns": [ + 356.72441 + ] + }, + "update": { + "median_ns": 1006.70005, + "min_ns": 1006.70005, + "max_ns": 1006.70005, + "samples_ns": [ + 1006.70005 + ] + }, + "delete": { + "median_ns": 779.56371, + "min_ns": 779.56371, + "max_ns": 779.56371, + "samples_ns": [ + 779.56371 + ] + } + } + } + ], + "run_url": "https://github.com/linksplatform/Comparisons.SQLiteVSDoublets/actions/runs/37226110636", + "date": "2026-10-04", + "machine": "Ubuntu 24.04.5 LTS, AMD EPYC 9V74 80-Core Processor, 4 cores, 16 GiB" +} diff --git a/docs/benchmarks/results/links-csharp-64-100000.json b/docs/benchmarks/results/links-csharp-64-100000.json new file mode 100644 index 0000000..d45cee3 --- /dev/null +++ b/docs/benchmarks/results/links-csharp-64-100000.json @@ -0,0 +1,866 @@ +{ + "language": "C#", + "category": "links", + "bits": 64, + "size": 100000, + "repetitions": 10, + "warm_up_size": 10000, + "sqlite_version": "3.53.3", + "results": [ + { + "variant": "SQLite_Memory", + "file_bytes": null, + "operations": { + "create": { + "median_ns": 4629.1975, + "min_ns": 4440.243, + "max_ns": 5550.816, + "samples_ns": [ + 5550.816, + 4575.297, + 4631.414, + 4440.243, + 4712.66, + 4627.964, + 4718.329, + 4584.293, + 4630.431, + 4543.844 + ] + }, + "query_all": { + "median_ns": 354.8415, + "min_ns": 336.271, + "max_ns": 366.139, + "samples_ns": [ + 362.596, + 354.095, + 366.139, + 336.695, + 359.247, + 355.588, + 363.352, + 337.13, + 337.239, + 336.271 + ] + }, + "query_by_id": { + "median_ns": 882.3054999999999, + "min_ns": 843.959, + "max_ns": 981.735, + "samples_ns": [ + 981.735, + 879.792, + 881.071, + 843.959, + 880.889, + 883.54, + 908.501, + 862.156, + 884.811, + 899.789 + ] + }, + "query_by_from_to": { + "median_ns": 1072.9969999999998, + "min_ns": 1042.49, + "max_ns": 1094.9, + "samples_ns": [ + 1094.9, + 1071.537, + 1090.391, + 1042.49, + 1072.724, + 1073.27, + 1074.024, + 1068.489, + 1073.694, + 1070.684 + ] + }, + "query_by_from": { + "median_ns": 1122.742, + "min_ns": 1075.25, + "max_ns": 1142.523, + "samples_ns": [ + 1142.523, + 1119.453, + 1121.895, + 1075.25, + 1102.661, + 1129.35, + 1129.347, + 1127.119, + 1123.589, + 1115.689 + ] + }, + "query_by_to": { + "median_ns": 1128.8600000000001, + "min_ns": 1080.754, + "max_ns": 1151.719, + "samples_ns": [ + 1149.022, + 1132.461, + 1125.259, + 1080.754, + 1107.625, + 1151.719, + 1137.886, + 1100.168, + 1132.618, + 1088.594 + ] + }, + "update": { + "median_ns": 3454.3514999999998, + "min_ns": 3303.697, + "max_ns": 3551.019, + "samples_ns": [ + 3489.161, + 3551.019, + 3507.467, + 3303.697, + 3399.472, + 3435.678, + 3454.181, + 3458.903, + 3454.522, + 3437.522 + ] + }, + "delete": { + "median_ns": 2296.7275, + "min_ns": 2247.143, + "max_ns": 2540.188, + "samples_ns": [ + 2303.276, + 2540.188, + 2247.143, + 2289.997, + 2385.341, + 2293.78, + 2311.82, + 2271.5, + 2291.148, + 2299.675 + ] + } + } + }, + { + "variant": "SQLite_File", + "file_bytes": 4620288, + "operations": { + "create": { + "median_ns": 3831.45, + "min_ns": 3684.072, + "max_ns": 10815.704, + "samples_ns": [ + 4415.535, + 3886.296, + 10815.704, + 3868.666, + 3873.894, + 3684.072, + 3794.234, + 3685.462, + 3782.936, + 3686.963 + ] + }, + "query_all": { + "median_ns": 362.9525, + "min_ns": 338.006, + "max_ns": 382.344, + "samples_ns": [ + 381.997, + 351.611, + 382.344, + 365.304, + 352.274, + 338.006, + 361.434, + 365.792, + 361.546, + 364.359 + ] + }, + "query_by_id": { + "median_ns": 843.565, + "min_ns": 826.594, + "max_ns": 893.474, + "samples_ns": [ + 893.437, + 893.474, + 883.241, + 849.179, + 826.594, + 833.735, + 838.331, + 842.57, + 844.56, + 840.094 + ] + }, + "query_by_from_to": { + "median_ns": 1027.5165000000002, + "min_ns": 1017.097, + "max_ns": 1065.886, + "samples_ns": [ + 1065.886, + 1058.153, + 1060.057, + 1045.745, + 1017.097, + 1017.714, + 1017.656, + 1025.105, + 1029.928, + 1019.499 + ] + }, + "query_by_from": { + "median_ns": 1066.274, + "min_ns": 1045.787, + "max_ns": 1111.432, + "samples_ns": [ + 1109.344, + 1079.288, + 1108.052, + 1111.432, + 1055.019, + 1066.938, + 1045.787, + 1061.403, + 1065.092, + 1065.61 + ] + }, + "query_by_to": { + "median_ns": 1079.4515, + "min_ns": 1052.828, + "max_ns": 1166.446, + "samples_ns": [ + 1109.599, + 1111.593, + 1166.446, + 1088.395, + 1070.307, + 1064.2, + 1052.828, + 1067.579, + 1080.206, + 1078.697 + ] + }, + "update": { + "median_ns": 7174.677, + "min_ns": 7035.524, + "max_ns": 11531.471, + "samples_ns": [ + 7339.011, + 11531.471, + 7311.047, + 7035.524, + 7147.219, + 9962.442, + 7066.982, + 7129.226, + 7122.961, + 7202.135 + ] + }, + "delete": { + "median_ns": 4037.6324999999997, + "min_ns": 3775.335, + "max_ns": 8141.208, + "samples_ns": [ + 4113.606, + 8141.208, + 4157.069, + 4169.349, + 3966.932, + 3873.472, + 3775.335, + 3778.319, + 3864.563, + 4108.333 + ] + } + } + }, + { + "variant": "Doublets_United_Volatile", + "file_bytes": null, + "operations": { + "create": { + "median_ns": 1124.622, + "min_ns": 1087.494, + "max_ns": 4117.043, + "samples_ns": [ + 4117.043, + 1087.494, + 1098.131, + 1125.468, + 1109.8, + 1139.953, + 1119.75, + 1123.776, + 1130.075, + 1213.323 + ] + }, + "query_all": { + "median_ns": 19.5675, + "min_ns": 16.849, + "max_ns": 20.761, + "samples_ns": [ + 16.849, + 18.833, + 18.755, + 19.525, + 19.61, + 20.207, + 19.146, + 20.631, + 19.91, + 20.761 + ] + }, + "query_by_id": { + "median_ns": 65.00049999999999, + "min_ns": 62.385, + "max_ns": 286.051, + "samples_ns": [ + 286.051, + 65.624, + 62.385, + 64.487, + 65.514, + 64.442, + 62.444, + 64.409, + 136.621, + 70.11 + ] + }, + "query_by_from_to": { + "median_ns": 222.9265, + "min_ns": 213.414, + "max_ns": 389.385, + "samples_ns": [ + 273.651, + 214.914, + 215.933, + 224.445, + 213.414, + 223.603, + 216.536, + 222.25, + 389.385, + 247.271 + ] + }, + "query_by_from": { + "median_ns": 265.462, + "min_ns": 252.291, + "max_ns": 443.852, + "samples_ns": [ + 315.021, + 254.208, + 263.435, + 264.218, + 252.291, + 270.752, + 258.239, + 266.706, + 443.852, + 324.621 + ] + }, + "query_by_to": { + "median_ns": 371.3195, + "min_ns": 290.577, + "max_ns": 394.291, + "samples_ns": [ + 361.005, + 365.68, + 369.16, + 377.431, + 373.991, + 373.479, + 364.403, + 377.494, + 394.291, + 290.577 + ] + }, + "update": { + "median_ns": 2486.464, + "min_ns": 2414.939, + "max_ns": 2523.196, + "samples_ns": [ + 2484.77, + 2414.939, + 2465.108, + 2490.482, + 2492.74, + 2437.369, + 2442.352, + 2488.158, + 2523.196, + 2492.844 + ] + }, + "delete": { + "median_ns": 1236.4065, + "min_ns": 1205.758, + "max_ns": 1332.66, + "samples_ns": [ + 1332.66, + 1205.758, + 1233.32, + 1232.705, + 1253.769, + 1228.728, + 1243.649, + 1235.618, + 1277.437, + 1237.195 + ] + } + } + }, + { + "variant": "Doublets_United_NonVolatile", + "file_bytes": 67108864, + "operations": { + "create": { + "median_ns": 1202.1305000000002, + "min_ns": 1166.778, + "max_ns": 1293.481, + "samples_ns": [ + 1203.351, + 1200.91, + 1166.778, + 1189.128, + 1173.765, + 1180.863, + 1225.159, + 1209.971, + 1293.481, + 1233.739 + ] + }, + "query_all": { + "median_ns": 20.807499999999997, + "min_ns": 20.245, + "max_ns": 23.857, + "samples_ns": [ + 20.496, + 21.003, + 20.311, + 21.037, + 20.245, + 20.34, + 20.612, + 22.698, + 22.706, + 23.857 + ] + }, + "query_by_id": { + "median_ns": 72.9665, + "min_ns": 68.546, + "max_ns": 82.302, + "samples_ns": [ + 73.917, + 71.717, + 73.57, + 71.759, + 68.546, + 73.879, + 69.341, + 82.302, + 81.606, + 72.363 + ] + }, + "query_by_from_to": { + "median_ns": 278.8895, + "min_ns": 267.285, + "max_ns": 289.739, + "samples_ns": [ + 280.646, + 279.573, + 267.285, + 274.456, + 267.771, + 277.532, + 278.206, + 283.544, + 289.739, + 282.672 + ] + }, + "query_by_from": { + "median_ns": 359.6965, + "min_ns": 352.836, + "max_ns": 412.608, + "samples_ns": [ + 357.586, + 353.712, + 353.529, + 358.84, + 352.836, + 360.553, + 361.814, + 363.277, + 412.608, + 369.786 + ] + }, + "query_by_to": { + "median_ns": 299.13300000000004, + "min_ns": 288.355, + "max_ns": 324.376, + "samples_ns": [ + 292.023, + 288.355, + 291.744, + 306.255, + 294.263, + 299.857, + 298.409, + 305.123, + 324.376, + 302.823 + ] + }, + "update": { + "median_ns": 2565.2565, + "min_ns": 2514.653, + "max_ns": 2668.346, + "samples_ns": [ + 2571.322, + 2544.186, + 2562.344, + 2514.653, + 2515.417, + 2568.169, + 2538.707, + 2601.507, + 2668.346, + 2641.493 + ] + }, + "delete": { + "median_ns": 1239.6055000000001, + "min_ns": 1202.495, + "max_ns": 1271.767, + "samples_ns": [ + 1248.927, + 1240.183, + 1229.761, + 1233.435, + 1205.686, + 1239.028, + 1202.495, + 1240.83, + 1263.209, + 1271.767 + ] + } + } + }, + { + "variant": "Doublets_Split_Volatile", + "file_bytes": null, + "operations": { + "create": { + "median_ns": 246.6335, + "min_ns": 226.476, + "max_ns": 518.77, + "samples_ns": [ + 518.77, + 260.325, + 263.087, + 262.569, + 239.29, + 236, + 238.54, + 235.044, + 253.977, + 226.476 + ] + }, + "query_all": { + "median_ns": 15.3585, + "min_ns": 12.968, + "max_ns": 20.188, + "samples_ns": [ + 20.188, + 12.968, + 16.573, + 17.103, + 15.023, + 15.092, + 15.9, + 14.093, + 15.251, + 15.466 + ] + }, + "query_by_id": { + "median_ns": 91.2075, + "min_ns": 89.342, + "max_ns": 226.746, + "samples_ns": [ + 226.746, + 90.653, + 89.569, + 91.005, + 95.637, + 91.526, + 93.48, + 89.342, + 91.41, + 90.777 + ] + }, + "query_by_from_to": { + "median_ns": 130.44299999999998, + "min_ns": 128.595, + "max_ns": 312.203, + "samples_ns": [ + 312.203, + 132.356, + 131.315, + 130.558, + 132.162, + 129.774, + 130.328, + 128.595, + 129.631, + 129.705 + ] + }, + "query_by_from": { + "median_ns": 117.74199999999999, + "min_ns": 113.885, + "max_ns": 320.335, + "samples_ns": [ + 320.335, + 118.256, + 117.315, + 122.676, + 120.778, + 116.866, + 118.169, + 114.49, + 113.885, + 115.088 + ] + }, + "query_by_to": { + "median_ns": 113.114, + "min_ns": 110.186, + "max_ns": 141.043, + "samples_ns": [ + 141.043, + 113.564, + 112.062, + 115.103, + 113.981, + 113.481, + 112.747, + 112.046, + 111.607, + 110.186 + ] + }, + "update": { + "median_ns": 233.6635, + "min_ns": 230.904, + "max_ns": 528.197, + "samples_ns": [ + 528.197, + 235.662, + 237.271, + 234.997, + 234.938, + 232.389, + 232.083, + 231.074, + 230.904, + 231.004 + ] + }, + "delete": { + "median_ns": 295.9785, + "min_ns": 285.848, + "max_ns": 358.204, + "samples_ns": [ + 358.204, + 297.939, + 300.822, + 295.728, + 294.616, + 301.689, + 294.579, + 296.229, + 290.658, + 285.848 + ] + } + } + }, + { + "variant": "Doublets_Split_NonVolatile", + "file_bytes": 83886080, + "operations": { + "create": { + "median_ns": 278.03049999999996, + "min_ns": 274.719, + "max_ns": 324.404, + "samples_ns": [ + 274.719, + 276.572, + 324.404, + 275.316, + 282.239, + 281.287, + 275.517, + 277.347, + 278.714, + 281.124 + ] + }, + "query_all": { + "median_ns": 13.982, + "min_ns": 13.192, + "max_ns": 17.545, + "samples_ns": [ + 13.192, + 13.966, + 17.545, + 13.886, + 14.158, + 13.883, + 13.729, + 13.998, + 14.113, + 14.421 + ] + }, + "query_by_id": { + "median_ns": 86.6225, + "min_ns": 83.733, + "max_ns": 117.786, + "samples_ns": [ + 84.833, + 88.453, + 117.786, + 87.078, + 86.724, + 87.064, + 83.888, + 83.733, + 85.436, + 86.521 + ] + }, + "query_by_from_to": { + "median_ns": 127.93299999999999, + "min_ns": 125.542, + "max_ns": 130.84, + "samples_ns": [ + 126.161, + 130.84, + 125.542, + 127.635, + 128.431, + 127.33, + 127.276, + 128.474, + 128.231, + 130.211 + ] + }, + "query_by_from": { + "median_ns": 118.117, + "min_ns": 112.403, + "max_ns": 121.679, + "samples_ns": [ + 114.936, + 118.165, + 112.403, + 118.749, + 118.069, + 116.898, + 117.485, + 119.106, + 118.515, + 121.679 + ] + }, + "query_by_to": { + "median_ns": 115.7775, + "min_ns": 113.691, + "max_ns": 117.295, + "samples_ns": [ + 116.038, + 115.225, + 113.691, + 114.278, + 116.048, + 115.922, + 115.633, + 115.434, + 117.295, + 116.899 + ] + }, + "update": { + "median_ns": 236.2145, + "min_ns": 234.624, + "max_ns": 240.499, + "samples_ns": [ + 240.499, + 238.418, + 235.833, + 236.267, + 239.918, + 237.999, + 235.946, + 235.195, + 234.624, + 236.162 + ] + }, + "delete": { + "median_ns": 265.6935, + "min_ns": 259.081, + "max_ns": 270.606, + "samples_ns": [ + 268.359, + 270.606, + 264.442, + 264.946, + 270.252, + 262.168, + 259.081, + 265.43, + 266.33, + 265.957 + ] + } + } + } + ], + "run_url": "https://github.com/linksplatform/Comparisons.SQLiteVSDoublets/actions/runs/37226110636", + "date": "2026-10-04", + "machine": "Ubuntu 24.04.5 LTS, Intel(R) Xeon(R) 6973P-C, 4 cores, 16 GiB" +} diff --git a/docs/benchmarks/results/links-csharp-64-1000000.json b/docs/benchmarks/results/links-csharp-64-1000000.json new file mode 100644 index 0000000..2418f99 --- /dev/null +++ b/docs/benchmarks/results/links-csharp-64-1000000.json @@ -0,0 +1,530 @@ +{ + "language": "C#", + "category": "links", + "bits": 64, + "size": 1000000, + "repetitions": 3, + "warm_up_size": 10000, + "sqlite_version": "3.53.3", + "results": [ + { + "variant": "SQLite_Memory", + "file_bytes": null, + "operations": { + "create": { + "median_ns": 7389.6285, + "min_ns": 7170.2148, + "max_ns": 7535.2732, + "samples_ns": [ + 7535.2732, + 7389.6285, + 7170.2148 + ] + }, + "query_all": { + "median_ns": 386.4072, + "min_ns": 385.6713, + "max_ns": 388.9548, + "samples_ns": [ + 386.4072, + 385.6713, + 388.9548 + ] + }, + "query_by_id": { + "median_ns": 1487.1982, + "min_ns": 1427.2554, + "max_ns": 1520.9275, + "samples_ns": [ + 1487.1982, + 1520.9275, + 1427.2554 + ] + }, + "query_by_from_to": { + "median_ns": 1800.0309, + "min_ns": 1742.1604, + "max_ns": 1846.1796, + "samples_ns": [ + 1800.0309, + 1846.1796, + 1742.1604 + ] + }, + "query_by_from": { + "median_ns": 1826.9007, + "min_ns": 1790.6512, + "max_ns": 1866.7377, + "samples_ns": [ + 1866.7377, + 1826.9007, + 1790.6512 + ] + }, + "query_by_to": { + "median_ns": 1833.1801, + "min_ns": 1794.3049, + "max_ns": 1842.6539, + "samples_ns": [ + 1842.6539, + 1833.1801, + 1794.3049 + ] + }, + "update": { + "median_ns": 7123.1822, + "min_ns": 6871.603, + "max_ns": 7286.4089, + "samples_ns": [ + 7286.4089, + 7123.1822, + 6871.603 + ] + }, + "delete": { + "median_ns": 4860.7295, + "min_ns": 4559.7462, + "max_ns": 5043.6842, + "samples_ns": [ + 5043.6842, + 4860.7295, + 4559.7462 + ] + } + } + }, + { + "variant": "SQLite_File", + "file_bytes": 50540544, + "operations": { + "create": { + "median_ns": 9968.8815, + "min_ns": 9921.1953, + "max_ns": 10015.6826, + "samples_ns": [ + 9921.1953, + 10015.6826, + 9968.8815 + ] + }, + "query_all": { + "median_ns": 385.9933, + "min_ns": 385.4248, + "max_ns": 386.9716, + "samples_ns": [ + 386.9716, + 385.9933, + 385.4248 + ] + }, + "query_by_id": { + "median_ns": 2637.6153, + "min_ns": 2626.6409, + "max_ns": 2641.5643, + "samples_ns": [ + 2637.6153, + 2641.5643, + 2626.6409 + ] + }, + "query_by_from_to": { + "median_ns": 2763.9996, + "min_ns": 2753.4576, + "max_ns": 2798.7362, + "samples_ns": [ + 2763.9996, + 2798.7362, + 2753.4576 + ] + }, + "query_by_from": { + "median_ns": 2840.6817, + "min_ns": 2833.0005, + "max_ns": 2844.4646, + "samples_ns": [ + 2844.4646, + 2840.6817, + 2833.0005 + ] + }, + "query_by_to": { + "median_ns": 2857.2559, + "min_ns": 2856.1801, + "max_ns": 2857.8929, + "samples_ns": [ + 2856.1801, + 2857.8929, + 2857.2559 + ] + }, + "update": { + "median_ns": 20953.0234, + "min_ns": 20891.0631, + "max_ns": 21134.5309, + "samples_ns": [ + 21134.5309, + 20891.0631, + 20953.0234 + ] + }, + "delete": { + "median_ns": 11713.3719, + "min_ns": 11650.5825, + "max_ns": 11751.3579, + "samples_ns": [ + 11713.3719, + 11751.3579, + 11650.5825 + ] + } + } + }, + { + "variant": "Doublets_United_Volatile", + "file_bytes": null, + "operations": { + "create": { + "median_ns": 2148.7525, + "min_ns": 2042.9875, + "max_ns": 2189.019, + "samples_ns": [ + 2189.019, + 2042.9875, + 2148.7525 + ] + }, + "query_all": { + "median_ns": 22.3499, + "min_ns": 21.1062, + "max_ns": 26.311, + "samples_ns": [ + 26.311, + 21.1062, + 22.3499 + ] + }, + "query_by_id": { + "median_ns": 162.136, + "min_ns": 159.1511, + "max_ns": 184.6899, + "samples_ns": [ + 184.6899, + 159.1511, + 162.136 + ] + }, + "query_by_from_to": { + "median_ns": 576.3611, + "min_ns": 571.1724, + "max_ns": 588.6539, + "samples_ns": [ + 576.3611, + 588.6539, + 571.1724 + ] + }, + "query_by_from": { + "median_ns": 686.7796, + "min_ns": 669.8893, + "max_ns": 694.4188, + "samples_ns": [ + 694.4188, + 686.7796, + 669.8893 + ] + }, + "query_by_to": { + "median_ns": 918.3445, + "min_ns": 897.283, + "max_ns": 919.0214, + "samples_ns": [ + 918.3445, + 919.0214, + 897.283 + ] + }, + "update": { + "median_ns": 5138.987, + "min_ns": 5050.9474, + "max_ns": 5151.773, + "samples_ns": [ + 5050.9474, + 5151.773, + 5138.987 + ] + }, + "delete": { + "median_ns": 2319.4211, + "min_ns": 2303.664, + "max_ns": 2367.5789, + "samples_ns": [ + 2303.664, + 2367.5789, + 2319.4211 + ] + } + } + }, + { + "variant": "Doublets_United_NonVolatile", + "file_bytes": 67108864, + "operations": { + "create": { + "median_ns": 2197.0063, + "min_ns": 2179.079, + "max_ns": 2215.1535, + "samples_ns": [ + 2197.0063, + 2215.1535, + 2179.079 + ] + }, + "query_all": { + "median_ns": 21.6333, + "min_ns": 21.4366, + "max_ns": 21.902, + "samples_ns": [ + 21.4366, + 21.902, + 21.6333 + ] + }, + "query_by_id": { + "median_ns": 168.8924, + "min_ns": 167.7434, + "max_ns": 175.8973, + "samples_ns": [ + 175.8973, + 167.7434, + 168.8924 + ] + }, + "query_by_from_to": { + "median_ns": 710.3771, + "min_ns": 680.934, + "max_ns": 741.2241, + "samples_ns": [ + 710.3771, + 680.934, + 741.2241 + ] + }, + "query_by_from": { + "median_ns": 825.7954, + "min_ns": 818.1125, + "max_ns": 879.9636, + "samples_ns": [ + 879.9636, + 818.1125, + 825.7954 + ] + }, + "query_by_to": { + "median_ns": 1009.7178, + "min_ns": 964.6297, + "max_ns": 1040.957, + "samples_ns": [ + 1009.7178, + 1040.957, + 964.6297 + ] + }, + "update": { + "median_ns": 5661.547, + "min_ns": 5599.2425, + "max_ns": 5767.6359, + "samples_ns": [ + 5767.6359, + 5599.2425, + 5661.547 + ] + }, + "delete": { + "median_ns": 2575.8412, + "min_ns": 2556.3078, + "max_ns": 2580.066, + "samples_ns": [ + 2575.8412, + 2580.066, + 2556.3078 + ] + } + } + }, + { + "variant": "Doublets_Split_Volatile", + "file_bytes": null, + "operations": { + "create": { + "median_ns": 366.4039, + "min_ns": 362.5751, + "max_ns": 477.0671, + "samples_ns": [ + 477.0671, + 366.4039, + 362.5751 + ] + }, + "query_all": { + "median_ns": 20.8015, + "min_ns": 12.8606, + "max_ns": 20.9036, + "samples_ns": [ + 12.8606, + 20.9036, + 20.8015 + ] + }, + "query_by_id": { + "median_ns": 220.3234, + "min_ns": 217.3853, + "max_ns": 228.4313, + "samples_ns": [ + 228.4313, + 220.3234, + 217.3853 + ] + }, + "query_by_from_to": { + "median_ns": 328.0933, + "min_ns": 325.4693, + "max_ns": 338.0268, + "samples_ns": [ + 328.0933, + 325.4693, + 338.0268 + ] + }, + "query_by_from": { + "median_ns": 280.4941, + "min_ns": 280.1256, + "max_ns": 282.7232, + "samples_ns": [ + 280.1256, + 280.4941, + 282.7232 + ] + }, + "query_by_to": { + "median_ns": 281.7755, + "min_ns": 281.2841, + "max_ns": 282.0112, + "samples_ns": [ + 282.0112, + 281.7755, + 281.2841 + ] + }, + "update": { + "median_ns": 658.1685, + "min_ns": 653.3468, + "max_ns": 661.8128, + "samples_ns": [ + 658.1685, + 653.3468, + 661.8128 + ] + }, + "delete": { + "median_ns": 566.8588, + "min_ns": 562.7661, + "max_ns": 568.266, + "samples_ns": [ + 562.7661, + 568.266, + 566.8588 + ] + } + } + }, + { + "variant": "Doublets_Split_NonVolatile", + "file_bytes": 83886080, + "operations": { + "create": { + "median_ns": 458.1437, + "min_ns": 448.6434, + "max_ns": 475.6127, + "samples_ns": [ + 458.1437, + 448.6434, + 475.6127 + ] + }, + "query_all": { + "median_ns": 21.195, + "min_ns": 20.9901, + "max_ns": 21.3843, + "samples_ns": [ + 20.9901, + 21.3843, + 21.195 + ] + }, + "query_by_id": { + "median_ns": 244.9139, + "min_ns": 236.927, + "max_ns": 251.3495, + "samples_ns": [ + 244.9139, + 236.927, + 251.3495 + ] + }, + "query_by_from_to": { + "median_ns": 369.3206, + "min_ns": 362.1006, + "max_ns": 399.0408, + "samples_ns": [ + 369.3206, + 362.1006, + 399.0408 + ] + }, + "query_by_from": { + "median_ns": 316.8218, + "min_ns": 314.0177, + "max_ns": 336.6497, + "samples_ns": [ + 316.8218, + 314.0177, + 336.6497 + ] + }, + "query_by_to": { + "median_ns": 323.2202, + "min_ns": 319.1773, + "max_ns": 326.5132, + "samples_ns": [ + 319.1773, + 323.2202, + 326.5132 + ] + }, + "update": { + "median_ns": 716.0179, + "min_ns": 700.8791, + "max_ns": 724.9463, + "samples_ns": [ + 700.8791, + 716.0179, + 724.9463 + ] + }, + "delete": { + "median_ns": 579.5615, + "min_ns": 574.1875, + "max_ns": 583.676, + "samples_ns": [ + 574.1875, + 583.676, + 579.5615 + ] + } + } + } + ], + "run_url": "https://github.com/linksplatform/Comparisons.SQLiteVSDoublets/actions/runs/37226110636", + "date": "2026-10-04", + "machine": "Ubuntu 24.04.5 LTS, AMD EPYC 7763 64-Core Processor, 4 cores, 16 GiB" +} diff --git a/docs/benchmarks/results/links-csharp-64-10000000.json b/docs/benchmarks/results/links-csharp-64-10000000.json new file mode 100644 index 0000000..10ead1f --- /dev/null +++ b/docs/benchmarks/results/links-csharp-64-10000000.json @@ -0,0 +1,434 @@ +{ + "language": "C#", + "category": "links", + "bits": 64, + "size": 10000000, + "repetitions": 1, + "warm_up_size": 10000, + "sqlite_version": "3.53.3", + "results": [ + { + "variant": "SQLite_Memory", + "file_bytes": null, + "operations": { + "create": { + "median_ns": 8404.77302, + "min_ns": 8404.77302, + "max_ns": 8404.77302, + "samples_ns": [ + 8404.77302 + ] + }, + "query_all": { + "median_ns": 383.5064, + "min_ns": 383.5064, + "max_ns": 383.5064, + "samples_ns": [ + 383.5064 + ] + }, + "query_by_id": { + "median_ns": 1886.5754, + "min_ns": 1886.5754, + "max_ns": 1886.5754, + "samples_ns": [ + 1886.5754 + ] + }, + "query_by_from_to": { + "median_ns": 2213.57006, + "min_ns": 2213.57006, + "max_ns": 2213.57006, + "samples_ns": [ + 2213.57006 + ] + }, + "query_by_from": { + "median_ns": 2183.13021, + "min_ns": 2183.13021, + "max_ns": 2183.13021, + "samples_ns": [ + 2183.13021 + ] + }, + "query_by_to": { + "median_ns": 2157.86208, + "min_ns": 2157.86208, + "max_ns": 2157.86208, + "samples_ns": [ + 2157.86208 + ] + }, + "update": { + "median_ns": 9449.51113, + "min_ns": 9449.51113, + "max_ns": 9449.51113, + "samples_ns": [ + 9449.51113 + ] + }, + "delete": { + "median_ns": 6626.34305, + "min_ns": 6626.34305, + "max_ns": 6626.34305, + "samples_ns": [ + 6626.34305 + ] + } + } + }, + { + "variant": "SQLite_File", + "file_bytes": 525307904, + "operations": { + "create": { + "median_ns": 20017.87816, + "min_ns": 20017.87816, + "max_ns": 20017.87816, + "samples_ns": [ + 20017.87816 + ] + }, + "query_all": { + "median_ns": 382.75514, + "min_ns": 382.75514, + "max_ns": 382.75514, + "samples_ns": [ + 382.75514 + ] + }, + "query_by_id": { + "median_ns": 2879.35144, + "min_ns": 2879.35144, + "max_ns": 2879.35144, + "samples_ns": [ + 2879.35144 + ] + }, + "query_by_from_to": { + "median_ns": 3377.68951, + "min_ns": 3377.68951, + "max_ns": 3377.68951, + "samples_ns": [ + 3377.68951 + ] + }, + "query_by_from": { + "median_ns": 3321.86606, + "min_ns": 3321.86606, + "max_ns": 3321.86606, + "samples_ns": [ + 3321.86606 + ] + }, + "query_by_to": { + "median_ns": 3322.65174, + "min_ns": 3322.65174, + "max_ns": 3322.65174, + "samples_ns": [ + 3322.65174 + ] + }, + "update": { + "median_ns": 25395.32328, + "min_ns": 25395.32328, + "max_ns": 25395.32328, + "samples_ns": [ + 25395.32328 + ] + }, + "delete": { + "median_ns": 15256.72265, + "min_ns": 15256.72265, + "max_ns": 15256.72265, + "samples_ns": [ + 15256.72265 + ] + } + } + }, + { + "variant": "Doublets_United_Volatile", + "file_bytes": null, + "operations": { + "create": { + "median_ns": 3039.76669, + "min_ns": 3039.76669, + "max_ns": 3039.76669, + "samples_ns": [ + 3039.76669 + ] + }, + "query_all": { + "median_ns": 13.85414, + "min_ns": 13.85414, + "max_ns": 13.85414, + "samples_ns": [ + 13.85414 + ] + }, + "query_by_id": { + "median_ns": 179.85085, + "min_ns": 179.85085, + "max_ns": 179.85085, + "samples_ns": [ + 179.85085 + ] + }, + "query_by_from_to": { + "median_ns": 1307.6519, + "min_ns": 1307.6519, + "max_ns": 1307.6519, + "samples_ns": [ + 1307.6519 + ] + }, + "query_by_from": { + "median_ns": 1501.39472, + "min_ns": 1501.39472, + "max_ns": 1501.39472, + "samples_ns": [ + 1501.39472 + ] + }, + "query_by_to": { + "median_ns": 1745.03354, + "min_ns": 1745.03354, + "max_ns": 1745.03354, + "samples_ns": [ + 1745.03354 + ] + }, + "update": { + "median_ns": 8602.05375, + "min_ns": 8602.05375, + "max_ns": 8602.05375, + "samples_ns": [ + 8602.05375 + ] + }, + "delete": { + "median_ns": 3842.42483, + "min_ns": 3842.42483, + "max_ns": 3842.42483, + "samples_ns": [ + 3842.42483 + ] + } + } + }, + { + "variant": "Doublets_United_NonVolatile", + "file_bytes": 671088640, + "operations": { + "create": { + "median_ns": 4426.89234, + "min_ns": 4426.89234, + "max_ns": 4426.89234, + "samples_ns": [ + 4426.89234 + ] + }, + "query_all": { + "median_ns": 21.57252, + "min_ns": 21.57252, + "max_ns": 21.57252, + "samples_ns": [ + 21.57252 + ] + }, + "query_by_id": { + "median_ns": 171.84868, + "min_ns": 171.84868, + "max_ns": 171.84868, + "samples_ns": [ + 171.84868 + ] + }, + "query_by_from_to": { + "median_ns": 1357.42421, + "min_ns": 1357.42421, + "max_ns": 1357.42421, + "samples_ns": [ + 1357.42421 + ] + }, + "query_by_from": { + "median_ns": 1548.76225, + "min_ns": 1548.76225, + "max_ns": 1548.76225, + "samples_ns": [ + 1548.76225 + ] + }, + "query_by_to": { + "median_ns": 1650.96449, + "min_ns": 1650.96449, + "max_ns": 1650.96449, + "samples_ns": [ + 1650.96449 + ] + }, + "update": { + "median_ns": 9639.50979, + "min_ns": 9639.50979, + "max_ns": 9639.50979, + "samples_ns": [ + 9639.50979 + ] + }, + "delete": { + "median_ns": 4784.72806, + "min_ns": 4784.72806, + "max_ns": 4784.72806, + "samples_ns": [ + 4784.72806 + ] + } + } + }, + { + "variant": "Doublets_Split_Volatile", + "file_bytes": null, + "operations": { + "create": { + "median_ns": 457.8224, + "min_ns": 457.8224, + "max_ns": 457.8224, + "samples_ns": [ + 457.8224 + ] + }, + "query_all": { + "median_ns": 12.99921, + "min_ns": 12.99921, + "max_ns": 12.99921, + "samples_ns": [ + 12.99921 + ] + }, + "query_by_id": { + "median_ns": 308.07467, + "min_ns": 308.07467, + "max_ns": 308.07467, + "samples_ns": [ + 308.07467 + ] + }, + "query_by_from_to": { + "median_ns": 445.15715, + "min_ns": 445.15715, + "max_ns": 445.15715, + "samples_ns": [ + 445.15715 + ] + }, + "query_by_from": { + "median_ns": 328.63049, + "min_ns": 328.63049, + "max_ns": 328.63049, + "samples_ns": [ + 328.63049 + ] + }, + "query_by_to": { + "median_ns": 349.5582, + "min_ns": 349.5582, + "max_ns": 349.5582, + "samples_ns": [ + 349.5582 + ] + }, + "update": { + "median_ns": 908.30393, + "min_ns": 908.30393, + "max_ns": 908.30393, + "samples_ns": [ + 908.30393 + ] + }, + "delete": { + "median_ns": 724.93505, + "min_ns": 724.93505, + "max_ns": 724.93505, + "samples_ns": [ + 724.93505 + ] + } + } + }, + { + "variant": "Doublets_Split_NonVolatile", + "file_bytes": 838860800, + "operations": { + "create": { + "median_ns": 1263.49177, + "min_ns": 1263.49177, + "max_ns": 1263.49177, + "samples_ns": [ + 1263.49177 + ] + }, + "query_all": { + "median_ns": 11.85306, + "min_ns": 11.85306, + "max_ns": 11.85306, + "samples_ns": [ + 11.85306 + ] + }, + "query_by_id": { + "median_ns": 325.79165, + "min_ns": 325.79165, + "max_ns": 325.79165, + "samples_ns": [ + 325.79165 + ] + }, + "query_by_from_to": { + "median_ns": 466.75408, + "min_ns": 466.75408, + "max_ns": 466.75408, + "samples_ns": [ + 466.75408 + ] + }, + "query_by_from": { + "median_ns": 332.19985, + "min_ns": 332.19985, + "max_ns": 332.19985, + "samples_ns": [ + 332.19985 + ] + }, + "query_by_to": { + "median_ns": 344.65814, + "min_ns": 344.65814, + "max_ns": 344.65814, + "samples_ns": [ + 344.65814 + ] + }, + "update": { + "median_ns": 1047.60423, + "min_ns": 1047.60423, + "max_ns": 1047.60423, + "samples_ns": [ + 1047.60423 + ] + }, + "delete": { + "median_ns": 874.97882, + "min_ns": 874.97882, + "max_ns": 874.97882, + "samples_ns": [ + 874.97882 + ] + } + } + } + ], + "run_url": "https://github.com/linksplatform/Comparisons.SQLiteVSDoublets/actions/runs/37226110636", + "date": "2026-10-04", + "machine": "Ubuntu 24.04.5 LTS, AMD EPYC 7763 64-Core Processor, 4 cores, 16 GiB" +} diff --git a/docs/benchmarks/results/links-rust-32-100000.json b/docs/benchmarks/results/links-rust-32-100000.json new file mode 100644 index 0000000..41dda25 --- /dev/null +++ b/docs/benchmarks/results/links-rust-32-100000.json @@ -0,0 +1,866 @@ +{ + "bits": 32, + "category": "links", + "language": "Rust", + "repetitions": 10, + "results": [ + { + "file_bytes": null, + "operations": { + "create": { + "max_ns": 1751.37263, + "median_ns": 1720.2500599999998, + "min_ns": 1711.83055, + "samples_ns": [ + 1748.11905, + 1714.60604, + 1726.66231, + 1711.83055, + 1716.6388, + 1721.00037, + 1715.82114, + 1751.37263, + 1720.67956, + 1719.82056 + ] + }, + "delete": { + "max_ns": 2177.19936, + "median_ns": 2166.1070600000003, + "min_ns": 2153.33729, + "samples_ns": [ + 2153.33729, + 2177.19936, + 2156.16054, + 2171.98349, + 2173.48926, + 2163.00542, + 2159.97147, + 2172.64969, + 2162.13924, + 2169.2087 + ] + }, + "query_all": { + "max_ns": 126.6373, + "median_ns": 125.10491999999999, + "min_ns": 123.02057, + "samples_ns": [ + 125.41996, + 126.6373, + 123.83528, + 123.89276, + 123.89476, + 126.18562, + 124.97763, + 123.02057, + 125.6877, + 125.23221 + ] + }, + "query_by_from": { + "max_ns": 771.68307, + "median_ns": 765.234235, + "min_ns": 756.24779, + "samples_ns": [ + 766.68562, + 768.0667, + 764.98936, + 764.20708, + 765.74159, + 756.24779, + 771.68307, + 763.00728, + 765.47911, + 757.65511 + ] + }, + "query_by_from_to": { + "max_ns": 709.67186, + "median_ns": 692.5007800000001, + "min_ns": 688.2623, + "samples_ns": [ + 696.18915, + 695.85324, + 688.79922, + 692.55196, + 694.3633, + 688.2623, + 689.64027, + 689.16946, + 709.67186, + 692.4496 + ] + }, + "query_by_id": { + "max_ns": 639.02157, + "median_ns": 605.919835, + "min_ns": 599.9934, + "samples_ns": [ + 626.42268, + 605.32588, + 639.02157, + 605.69875, + 606.14092, + 610.12622, + 599.9934, + 603.06654, + 624.70055, + 603.78863 + ] + }, + "query_by_to": { + "max_ns": 778.7408, + "median_ns": 766.077305, + "min_ns": 761.53466, + "samples_ns": [ + 772.32051, + 762.04822, + 762.57793, + 767.6036, + 775.65254, + 762.86516, + 764.55101, + 769.5155, + 778.7408, + 761.53466 + ] + }, + "update": { + "max_ns": 3403.93409, + "median_ns": 3361.70727, + "min_ns": 3306.46093, + "samples_ns": [ + 3306.46093, + 3347.54567, + 3392.50905, + 3382.78028, + 3362.8902, + 3351.31876, + 3352.80431, + 3389.07118, + 3403.93409, + 3360.52434 + ] + } + }, + "variant": "SQLite_Memory" + }, + { + "file_bytes": 4620288, + "operations": { + "create": { + "max_ns": 2137.81007, + "median_ns": 2088.94593, + "min_ns": 2075.64887, + "samples_ns": [ + 2088.97574, + 2081.6567, + 2075.64887, + 2076.83054, + 2090.24937, + 2086.73279, + 2088.91612, + 2137.81007, + 2090.98696, + 2104.27254 + ] + }, + "delete": { + "max_ns": 6121.64907, + "median_ns": 6040.548305, + "min_ns": 5956.59056, + "samples_ns": [ + 6121.64907, + 5956.59056, + 5979.82558, + 5978.81606, + 6004.47037, + 6030.79094, + 6082.11534, + 6074.80832, + 6050.30567, + 6078.03362 + ] + }, + "query_all": { + "max_ns": 130.53389, + "median_ns": 128.35498, + "min_ns": 127.39454, + "samples_ns": [ + 129.52618, + 127.97495, + 128.28378, + 128.57211, + 127.39454, + 128.42618, + 129.87276, + 127.73615, + 128.19423, + 130.53389 + ] + }, + "query_by_from": { + "max_ns": 801.20305, + "median_ns": 781.5294650000001, + "min_ns": 778.01371, + "samples_ns": [ + 778.01371, + 781.68555, + 801.20305, + 782.56946, + 781.32979, + 779.89267, + 778.40943, + 784.80868, + 783.14037, + 781.37338 + ] + }, + "query_by_from_to": { + "max_ns": 791.38721, + "median_ns": 726.87626, + "min_ns": 720.91202, + "samples_ns": [ + 727.37015, + 721.11251, + 721.40777, + 729.56476, + 720.91202, + 727.64147, + 725.98797, + 791.38721, + 726.38237, + 744.44871 + ] + }, + "query_by_id": { + "max_ns": 625.2204, + "median_ns": 621.3272099999999, + "min_ns": 617.39517, + "samples_ns": [ + 623.36164, + 621.2092, + 620.68242, + 624.08245, + 617.39517, + 624.47498, + 621.44522, + 625.2204, + 621.02759, + 620.90576 + ] + }, + "query_by_to": { + "max_ns": 797.32897, + "median_ns": 790.320665, + "min_ns": 775.90641, + "samples_ns": [ + 784.58191, + 789.04464, + 775.90641, + 792.58867, + 794.09613, + 784.87488, + 797.32897, + 792.30218, + 786.53919, + 791.59669 + ] + }, + "update": { + "max_ns": 12545.51676, + "median_ns": 12276.143145, + "min_ns": 12081.79899, + "samples_ns": [ + 12467.73443, + 12184.67139, + 12167.35696, + 12081.79899, + 12128.48647, + 12262.23649, + 12356.61081, + 12446.03147, + 12290.0498, + 12545.51676 + ] + } + }, + "variant": "SQLite_File" + }, + { + "file_bytes": null, + "operations": { + "create": { + "max_ns": 470.15379, + "median_ns": 457.81248, + "min_ns": 450.82395, + "samples_ns": [ + 459.486, + 462.25789, + 470.15379, + 458.11494, + 457.8907, + 457.73426, + 450.82395, + 457.11804, + 453.3114, + 452.44129 + ] + }, + "delete": { + "max_ns": 464.26481, + "median_ns": 458.681095, + "min_ns": 455.75959, + "samples_ns": [ + 457.99366, + 464.26481, + 457.45284, + 462.45448, + 455.75959, + 457.17472, + 458.30583, + 459.65666, + 459.05636, + 459.12656 + ] + }, + "query_all": { + "max_ns": 2.1224, + "median_ns": 1.905575, + "min_ns": 1.89706, + "samples_ns": [ + 1.90447, + 1.90747, + 2.1224, + 1.90668, + 1.89916, + 2.01634, + 1.89836, + 1.90668, + 1.89706, + 1.90107 + ] + }, + "query_by_from": { + "max_ns": 259.68569, + "median_ns": 247.66753, + "min_ns": 239.84959, + "samples_ns": [ + 241.14064, + 244.99204, + 252.91879, + 239.84959, + 248.51616, + 246.8189, + 259.68569, + 246.04243, + 258.47928, + 253.41214 + ] + }, + "query_by_from_to": { + "max_ns": 205.08438, + "median_ns": 194.16842000000003, + "min_ns": 190.08186, + "samples_ns": [ + 190.08186, + 190.53725, + 198.28242, + 192.33636, + 192.10882, + 205.08438, + 196.00048, + 199.17037, + 197.56194, + 191.74176 + ] + }, + "query_by_id": { + "max_ns": 6.74377, + "median_ns": 5.719519999999999, + "min_ns": 5.48037, + "samples_ns": [ + 5.6416, + 6.72264, + 5.71491, + 6.28998, + 5.72413, + 5.48037, + 6.60626, + 5.59984, + 6.74377, + 5.58832 + ] + }, + "query_by_to": { + "max_ns": 306.80733, + "median_ns": 267.46377, + "min_ns": 259.77103, + "samples_ns": [ + 266.30227, + 274.00218, + 268.62527, + 272.16952, + 262.40149, + 264.84918, + 306.80733, + 285.04262, + 259.77103, + 263.89995 + ] + }, + "update": { + "max_ns": 1248.7648, + "median_ns": 1236.364005, + "min_ns": 1221.78853, + "samples_ns": [ + 1221.78853, + 1243.03597, + 1223.84223, + 1236.0265, + 1226.60389, + 1248.7648, + 1236.70151, + 1246.20304, + 1224.11013, + 1241.84067 + ] + } + }, + "variant": "Doublets_United_Volatile" + }, + { + "file_bytes": 33554432, + "operations": { + "create": { + "max_ns": 517.68818, + "median_ns": 498.762465, + "min_ns": 490.203, + "samples_ns": [ + 504.81414, + 498.47589, + 497.71194, + 495.52622, + 499.04904, + 517.68818, + 492.97748, + 515.45561, + 490.203, + 503.81924 + ] + }, + "delete": { + "max_ns": 494.61245, + "median_ns": 483.78502000000003, + "min_ns": 480.01621, + "samples_ns": [ + 484.72355, + 483.23341, + 480.01621, + 487.63551, + 494.61245, + 482.08043, + 480.52738, + 485.87816, + 484.33663, + 482.12229 + ] + }, + "query_all": { + "max_ns": 2.11109, + "median_ns": 1.907375, + "min_ns": 1.89786, + "samples_ns": [ + 1.90868, + 1.93592, + 1.90607, + 1.89786, + 2.11109, + 1.91159, + 2.01734, + 1.90057, + 1.90147, + 1.90578 + ] + }, + "query_by_from": { + "max_ns": 288.80497, + "median_ns": 278.30164, + "min_ns": 270.23614, + "samples_ns": [ + 284.67156, + 276.70305, + 271.78534, + 270.23614, + 273.01203, + 285.5809, + 279.90023, + 288.80497, + 280.04685, + 276.27866 + ] + }, + "query_by_from_to": { + "max_ns": 219.13516, + "median_ns": 214.15433000000002, + "min_ns": 207.20428, + "samples_ns": [ + 212.57738, + 215.29087, + 219.13516, + 209.12799, + 207.20428, + 217.78095, + 215.14267, + 213.16599, + 212.48365, + 218.08411 + ] + }, + "query_by_id": { + "max_ns": 6.17773, + "median_ns": 5.879675000000001, + "min_ns": 5.75469, + "samples_ns": [ + 5.84601, + 6.02378, + 5.87406, + 5.88529, + 5.79344, + 5.96791, + 6.17773, + 5.75469, + 5.80807, + 6.14508 + ] + }, + "query_by_to": { + "max_ns": 296.23679, + "median_ns": 287.790785, + "min_ns": 281.05878, + "samples_ns": [ + 291.17777, + 288.89373, + 286.13027, + 288.38884, + 296.23679, + 287.29749, + 281.05878, + 288.28408, + 283.90217, + 284.2613 + ] + }, + "update": { + "max_ns": 1305.10033, + "median_ns": 1289.965565, + "min_ns": 1283.50452, + "samples_ns": [ + 1290.28519, + 1302.64924, + 1290.16185, + 1283.50452, + 1297.43195, + 1285.25106, + 1305.10033, + 1285.37165, + 1289.76928, + 1287.5335 + ] + } + }, + "variant": "Doublets_United_NonVolatile" + }, + { + "file_bytes": null, + "operations": { + "create": { + "max_ns": 104.19599, + "median_ns": 97.091815, + "min_ns": 95.04648, + "samples_ns": [ + 98.99454, + 97.69377, + 95.1274, + 95.64518, + 99.44592, + 95.04648, + 104.19599, + 97.79403, + 96.48986, + 95.70257 + ] + }, + "delete": { + "max_ns": 622.39774, + "median_ns": 607.3000549999999, + "min_ns": 598.47263, + "samples_ns": [ + 621.27014, + 604.53498, + 598.47263, + 622.39774, + 607.98699, + 605.78947, + 604.68881, + 610.40574, + 607.66541, + 606.9347 + ] + }, + "query_all": { + "max_ns": 3.73805, + "median_ns": 3.68496, + "min_ns": 3.58131, + "samples_ns": [ + 3.67485, + 3.58131, + 3.73224, + 3.58241, + 3.58211, + 3.73805, + 3.68496, + 3.68496, + 3.68666, + 3.68536 + ] + }, + "query_by_from": { + "max_ns": 30.404, + "median_ns": 29.43028, + "min_ns": 28.82481, + "samples_ns": [ + 28.82481, + 29.45056, + 28.91114, + 29.41, + 29.35942, + 29.00128, + 29.52437, + 29.64885, + 30.404, + 29.92147 + ] + }, + "query_by_from_to": { + "max_ns": 59.53978, + "median_ns": 58.716584999999995, + "min_ns": 56.34305, + "samples_ns": [ + 59.26626, + 57.31993, + 59.53978, + 56.76018, + 59.51754, + 58.70301, + 57.46384, + 56.34305, + 58.91643, + 58.73016 + ] + }, + "query_by_id": { + "max_ns": 6.87849, + "median_ns": 5.559495, + "min_ns": 5.51422, + "samples_ns": [ + 5.54417, + 5.72143, + 5.53796, + 5.57482, + 5.67647, + 5.52984, + 5.52545, + 5.75268, + 6.87849, + 5.51422 + ] + }, + "query_by_to": { + "max_ns": 32.00422, + "median_ns": 30.051765, + "min_ns": 29.40359, + "samples_ns": [ + 30.48461, + 30.32848, + 32.00422, + 30.53069, + 29.40359, + 29.68781, + 30.3423, + 29.70073, + 29.77505, + 29.49112 + ] + }, + "update": { + "max_ns": 176.65317, + "median_ns": 173.8308, + "min_ns": 168.51509, + "samples_ns": [ + 168.51509, + 175.40398, + 172.9709, + 171.07054, + 175.11825, + 175.78526, + 174.6907, + 171.9075, + 170.35947, + 176.65317 + ] + } + }, + "variant": "Doublets_Split_Volatile" + }, + { + "file_bytes": 41943040, + "operations": { + "create": { + "max_ns": 125.0041, + "median_ns": 118.65912499999999, + "min_ns": 103.15231, + "samples_ns": [ + 103.6659, + 103.15231, + 119.19063, + 119.59734, + 116.69276, + 125.0041, + 122.33908, + 103.45598, + 121.40417, + 118.12762 + ] + }, + "delete": { + "max_ns": 653.9663, + "median_ns": 648.931595, + "min_ns": 644.27447, + "samples_ns": [ + 644.27447, + 647.70285, + 652.0459, + 653.5705, + 644.66125, + 648.45207, + 653.9663, + 645.46767, + 649.85189, + 649.41112 + ] + }, + "query_all": { + "max_ns": 3.68266, + "median_ns": 3.5656800000000004, + "min_ns": 3.55166, + "samples_ns": [ + 3.56538, + 3.67415, + 3.68266, + 3.56057, + 3.5736, + 3.55166, + 3.56789, + 3.55727, + 3.56598, + 3.55417 + ] + }, + "query_by_from": { + "max_ns": 31.4703, + "median_ns": 31.15077, + "min_ns": 30.78177, + "samples_ns": [ + 31.29234, + 31.0705, + 30.78177, + 31.46049, + 31.23104, + 31.35754, + 30.86369, + 31.4703, + 30.96044, + 30.999 + ] + }, + "query_by_from_to": { + "max_ns": 62.43906, + "median_ns": 61.756875, + "min_ns": 61.49053, + "samples_ns": [ + 61.49053, + 61.56013, + 62.04987, + 61.97165, + 61.68722, + 61.72878, + 62.43906, + 61.58367, + 61.78497, + 62.21632 + ] + }, + "query_by_id": { + "max_ns": 7.77284, + "median_ns": 7.430165, + "min_ns": 6.19535, + "samples_ns": [ + 6.20667, + 6.40236, + 7.50453, + 7.77284, + 6.92836, + 7.65756, + 7.4876, + 6.19535, + 7.53037, + 7.37273 + ] + }, + "query_by_to": { + "max_ns": 32.42776, + "median_ns": 31.963904999999997, + "min_ns": 31.73701, + "samples_ns": [ + 31.88834, + 32.26791, + 31.73701, + 32.34653, + 32.02144, + 32.09705, + 31.81703, + 31.90637, + 31.87742, + 32.42776 + ] + }, + "update": { + "max_ns": 185.94189, + "median_ns": 183.72478999999998, + "min_ns": 182.20636, + "samples_ns": [ + 183.01737, + 182.20636, + 184.18483, + 183.6957, + 183.60025, + 184.72225, + 184.9571, + 182.73975, + 183.75388, + 185.94189 + ] + } + }, + "variant": "Doublets_Split_NonVolatile" + } + ], + "size": 100000, + "sqlite_version": "3.53.2", + "warm_up_size": 10000, + "run_url": "https://github.com/linksplatform/Comparisons.SQLiteVSDoublets/actions/runs/37226110636", + "date": "2026-10-04", + "machine": "Ubuntu 24.04.5 LTS, AMD EPYC 9V74 80-Core Processor, 4 cores, 16 GiB" +} diff --git a/docs/benchmarks/results/links-rust-32-1000000.json b/docs/benchmarks/results/links-rust-32-1000000.json new file mode 100644 index 0000000..9c9b52e --- /dev/null +++ b/docs/benchmarks/results/links-rust-32-1000000.json @@ -0,0 +1,530 @@ +{ + "bits": 32, + "category": "links", + "language": "Rust", + "repetitions": 3, + "results": [ + { + "file_bytes": null, + "operations": { + "create": { + "max_ns": 2430.95312, + "median_ns": 2390.484756, + "min_ns": 2350.062266, + "samples_ns": [ + 2390.484756, + 2430.95312, + 2350.062266 + ] + }, + "delete": { + "max_ns": 4653.037322, + "median_ns": 4258.612656, + "min_ns": 3636.551416, + "samples_ns": [ + 4653.037322, + 4258.612656, + 3636.551416 + ] + }, + "query_all": { + "max_ns": 124.756407, + "median_ns": 124.322739, + "min_ns": 124.09502, + "samples_ns": [ + 124.322739, + 124.09502, + 124.756407 + ] + }, + "query_by_from": { + "max_ns": 1053.820435, + "median_ns": 997.12567, + "min_ns": 991.200631, + "samples_ns": [ + 997.12567, + 1053.820435, + 991.200631 + ] + }, + "query_by_from_to": { + "max_ns": 1030.355053, + "median_ns": 982.783836, + "min_ns": 947.507521, + "samples_ns": [ + 982.783836, + 1030.355053, + 947.507521 + ] + }, + "query_by_id": { + "max_ns": 908.119152, + "median_ns": 874.442815, + "min_ns": 845.4826, + "samples_ns": [ + 874.442815, + 908.119152, + 845.4826 + ] + }, + "query_by_to": { + "max_ns": 1034.354, + "median_ns": 999.919786, + "min_ns": 922.042117, + "samples_ns": [ + 1034.354, + 999.919786, + 922.042117 + ] + }, + "update": { + "max_ns": 6518.163219, + "median_ns": 6484.433059, + "min_ns": 5648.484663, + "samples_ns": [ + 6518.163219, + 6484.433059, + 5648.484663 + ] + } + }, + "variant": "SQLite_Memory" + }, + { + "file_bytes": 50540544, + "operations": { + "create": { + "max_ns": 5836.643204, + "median_ns": 5758.259631, + "min_ns": 5748.658634, + "samples_ns": [ + 5748.658634, + 5836.643204, + 5758.259631 + ] + }, + "delete": { + "max_ns": 10642.343081, + "median_ns": 10622.379079, + "min_ns": 10504.654693, + "samples_ns": [ + 10642.343081, + 10622.379079, + 10504.654693 + ] + }, + "query_all": { + "max_ns": 127.810284, + "median_ns": 127.616712, + "min_ns": 127.505711, + "samples_ns": [ + 127.505711, + 127.810284, + 127.616712 + ] + }, + "query_by_from": { + "max_ns": 1925.771283, + "median_ns": 1920.185129, + "min_ns": 1917.399674, + "samples_ns": [ + 1925.771283, + 1917.399674, + 1920.185129 + ] + }, + "query_by_from_to": { + "max_ns": 1925.730542, + "median_ns": 1899.670479, + "min_ns": 1878.196487, + "samples_ns": [ + 1925.730542, + 1899.670479, + 1878.196487 + ] + }, + "query_by_id": { + "max_ns": 1969.179916, + "median_ns": 1959.001202, + "min_ns": 1952.023205, + "samples_ns": [ + 1952.023205, + 1969.179916, + 1959.001202 + ] + }, + "query_by_to": { + "max_ns": 1911.400095, + "median_ns": 1910.703726, + "min_ns": 1867.43875, + "samples_ns": [ + 1867.43875, + 1911.400095, + 1910.703726 + ] + }, + "update": { + "max_ns": 19163.192312, + "median_ns": 19035.891382, + "min_ns": 18817.65209, + "samples_ns": [ + 18817.65209, + 19035.891382, + 19163.192312 + ] + } + }, + "variant": "SQLite_File" + }, + { + "file_bytes": null, + "operations": { + "create": { + "max_ns": 756.983783, + "median_ns": 755.371199, + "min_ns": 728.622543, + "samples_ns": [ + 756.983783, + 755.371199, + 728.622543 + ] + }, + "delete": { + "max_ns": 950.346744, + "median_ns": 828.435554, + "min_ns": 825.748729, + "samples_ns": [ + 828.435554, + 825.748729, + 950.346744 + ] + }, + "query_all": { + "max_ns": 1.915414, + "median_ns": 1.841688, + "min_ns": 1.830423, + "samples_ns": [ + 1.830423, + 1.915414, + 1.841688 + ] + }, + "query_by_from": { + "max_ns": 598.222723, + "median_ns": 579.44325, + "min_ns": 554.5737, + "samples_ns": [ + 579.44325, + 554.5737, + 598.222723 + ] + }, + "query_by_from_to": { + "max_ns": 452.960349, + "median_ns": 417.204733, + "min_ns": 415.533929, + "samples_ns": [ + 452.960349, + 415.533929, + 417.204733 + ] + }, + "query_by_id": { + "max_ns": 20.771257, + "median_ns": 20.739774, + "min_ns": 20.560441, + "samples_ns": [ + 20.560441, + 20.771257, + 20.739774 + ] + }, + "query_by_to": { + "max_ns": 700.794028, + "median_ns": 619.732877, + "min_ns": 582.466203, + "samples_ns": [ + 582.466203, + 619.732877, + 700.794028 + ] + }, + "update": { + "max_ns": 2470.565355, + "median_ns": 2393.333324, + "min_ns": 2354.868868, + "samples_ns": [ + 2393.333324, + 2354.868868, + 2470.565355 + ] + } + }, + "variant": "Doublets_United_Volatile" + }, + { + "file_bytes": 33554432, + "operations": { + "create": { + "max_ns": 937.39891, + "median_ns": 867.761888, + "min_ns": 856.539532, + "samples_ns": [ + 856.539532, + 937.39891, + 867.761888 + ] + }, + "delete": { + "max_ns": 951.12108, + "median_ns": 914.269488, + "min_ns": 910.765763, + "samples_ns": [ + 914.269488, + 951.12108, + 910.765763 + ] + }, + "query_all": { + "max_ns": 2.220397, + "median_ns": 2.108879, + "min_ns": 2.078799, + "samples_ns": [ + 2.220397, + 2.078799, + 2.108879 + ] + }, + "query_by_from": { + "max_ns": 715.283165, + "median_ns": 680.767765, + "min_ns": 661.914623, + "samples_ns": [ + 680.767765, + 715.283165, + 661.914623 + ] + }, + "query_by_from_to": { + "max_ns": 562.54236, + "median_ns": 501.853908, + "min_ns": 479.661179, + "samples_ns": [ + 501.853908, + 479.661179, + 562.54236 + ] + }, + "query_by_id": { + "max_ns": 23.048094, + "median_ns": 22.694468, + "min_ns": 21.875246, + "samples_ns": [ + 23.048094, + 22.694468, + 21.875246 + ] + }, + "query_by_to": { + "max_ns": 709.399928, + "median_ns": 688.544072, + "min_ns": 671.60954, + "samples_ns": [ + 688.544072, + 709.399928, + 671.60954 + ] + }, + "update": { + "max_ns": 2825.923121, + "median_ns": 2617.52345, + "min_ns": 2612.536771, + "samples_ns": [ + 2612.536771, + 2825.923121, + 2617.52345 + ] + } + }, + "variant": "Doublets_United_NonVolatile" + }, + { + "file_bytes": null, + "operations": { + "create": { + "max_ns": 162.991704, + "median_ns": 162.207624, + "min_ns": 159.162972, + "samples_ns": [ + 162.991704, + 159.162972, + 162.207624 + ] + }, + "delete": { + "max_ns": 1207.649238, + "median_ns": 1198.430542, + "min_ns": 1131.784101, + "samples_ns": [ + 1131.784101, + 1207.649238, + 1198.430542 + ] + }, + "query_all": { + "max_ns": 3.880558, + "median_ns": 3.87159, + "min_ns": 3.826577, + "samples_ns": [ + 3.880558, + 3.826577, + 3.87159 + ] + }, + "query_by_from": { + "max_ns": 116.78273, + "median_ns": 116.249327, + "min_ns": 110.543842, + "samples_ns": [ + 116.78273, + 110.543842, + 116.249327 + ] + }, + "query_by_from_to": { + "max_ns": 165.132181, + "median_ns": 158.636911, + "min_ns": 158.055761, + "samples_ns": [ + 165.132181, + 158.636911, + 158.055761 + ] + }, + "query_by_id": { + "max_ns": 23.156949, + "median_ns": 22.973463, + "min_ns": 22.924325, + "samples_ns": [ + 23.156949, + 22.924325, + 22.973463 + ] + }, + "query_by_to": { + "max_ns": 112.632552, + "median_ns": 111.57795, + "min_ns": 110.492627, + "samples_ns": [ + 111.57795, + 112.632552, + 110.492627 + ] + }, + "update": { + "max_ns": 478.575445, + "median_ns": 457.42206, + "min_ns": 440.220136, + "samples_ns": [ + 457.42206, + 478.575445, + 440.220136 + ] + } + }, + "variant": "Doublets_Split_Volatile" + }, + { + "file_bytes": 41943040, + "operations": { + "create": { + "max_ns": 185.275614, + "median_ns": 173.730043, + "min_ns": 172.859274, + "samples_ns": [ + 173.730043, + 185.275614, + 172.859274 + ] + }, + "delete": { + "max_ns": 1436.678232, + "median_ns": 1415.434683, + "min_ns": 1378.314078, + "samples_ns": [ + 1436.678232, + 1415.434683, + 1378.314078 + ] + }, + "query_all": { + "max_ns": 3.948413, + "median_ns": 3.905762, + "min_ns": 3.896207, + "samples_ns": [ + 3.896207, + 3.948413, + 3.905762 + ] + }, + "query_by_from": { + "max_ns": 129.196396, + "median_ns": 129.022811, + "min_ns": 117.234631, + "samples_ns": [ + 129.022811, + 129.196396, + 117.234631 + ] + }, + "query_by_from_to": { + "max_ns": 179.425709, + "median_ns": 167.199927, + "min_ns": 160.281731, + "samples_ns": [ + 167.199927, + 179.425709, + 160.281731 + ] + }, + "query_by_id": { + "max_ns": 26.264589, + "median_ns": 24.916432, + "min_ns": 24.184664, + "samples_ns": [ + 24.916432, + 26.264589, + 24.184664 + ] + }, + "query_by_to": { + "max_ns": 130.814928, + "median_ns": 124.147566, + "min_ns": 120.90277, + "samples_ns": [ + 124.147566, + 130.814928, + 120.90277 + ] + }, + "update": { + "max_ns": 530.812819, + "median_ns": 493.735897, + "min_ns": 473.672422, + "samples_ns": [ + 493.735897, + 530.812819, + 473.672422 + ] + } + }, + "variant": "Doublets_Split_NonVolatile" + } + ], + "size": 1000000, + "sqlite_version": "3.53.2", + "warm_up_size": 10000, + "run_url": "https://github.com/linksplatform/Comparisons.SQLiteVSDoublets/actions/runs/37226110636", + "date": "2026-10-04", + "machine": "Ubuntu 24.04.5 LTS, AMD EPYC 7763 64-Core Processor, 4 cores, 16 GiB" +} diff --git a/docs/benchmarks/results/links-rust-32-10000000.json b/docs/benchmarks/results/links-rust-32-10000000.json new file mode 100644 index 0000000..2e4e4b8 --- /dev/null +++ b/docs/benchmarks/results/links-rust-32-10000000.json @@ -0,0 +1,434 @@ +{ + "bits": 32, + "category": "links", + "language": "Rust", + "repetitions": 1, + "results": [ + { + "file_bytes": null, + "operations": { + "create": { + "max_ns": 3341.7402931, + "median_ns": 3341.7402931, + "min_ns": 3341.7402931, + "samples_ns": [ + 3341.7402931 + ] + }, + "delete": { + "max_ns": 5586.6891587, + "median_ns": 5586.6891587, + "min_ns": 5586.6891587, + "samples_ns": [ + 5586.6891587 + ] + }, + "query_all": { + "max_ns": 123.5647568, + "median_ns": 123.5647568, + "min_ns": 123.5647568, + "samples_ns": [ + 123.5647568 + ] + }, + "query_by_from": { + "max_ns": 1505.2954064, + "median_ns": 1505.2954064, + "min_ns": 1505.2954064, + "samples_ns": [ + 1505.2954064 + ] + }, + "query_by_from_to": { + "max_ns": 1473.8611601, + "median_ns": 1473.8611601, + "min_ns": 1473.8611601, + "samples_ns": [ + 1473.8611601 + ] + }, + "query_by_id": { + "max_ns": 1390.7592012, + "median_ns": 1390.7592012, + "min_ns": 1390.7592012, + "samples_ns": [ + 1390.7592012 + ] + }, + "query_by_to": { + "max_ns": 1488.8709352, + "median_ns": 1488.8709352, + "min_ns": 1488.8709352, + "samples_ns": [ + 1488.8709352 + ] + }, + "update": { + "max_ns": 8531.1460793, + "median_ns": 8531.1460793, + "min_ns": 8531.1460793, + "samples_ns": [ + 8531.1460793 + ] + } + }, + "variant": "SQLite_Memory" + }, + { + "file_bytes": 525307904, + "operations": { + "create": { + "max_ns": 9224.0617118, + "median_ns": 9224.0617118, + "min_ns": 9224.0617118, + "samples_ns": [ + 9224.0617118 + ] + }, + "delete": { + "max_ns": 14461.3076623, + "median_ns": 14461.3076623, + "min_ns": 14461.3076623, + "samples_ns": [ + 14461.3076623 + ] + }, + "query_all": { + "max_ns": 126.5548585, + "median_ns": 126.5548585, + "min_ns": 126.5548585, + "samples_ns": [ + 126.5548585 + ] + }, + "query_by_from": { + "max_ns": 2555.6004353, + "median_ns": 2555.6004353, + "min_ns": 2555.6004353, + "samples_ns": [ + 2555.6004353 + ] + }, + "query_by_from_to": { + "max_ns": 2570.4034104, + "median_ns": 2570.4034104, + "min_ns": 2570.4034104, + "samples_ns": [ + 2570.4034104 + ] + }, + "query_by_id": { + "max_ns": 2178.7984706, + "median_ns": 2178.7984706, + "min_ns": 2178.7984706, + "samples_ns": [ + 2178.7984706 + ] + }, + "query_by_to": { + "max_ns": 2532.1027822, + "median_ns": 2532.1027822, + "min_ns": 2532.1027822, + "samples_ns": [ + 2532.1027822 + ] + }, + "update": { + "max_ns": 24045.2210295, + "median_ns": 24045.2210295, + "min_ns": 24045.2210295, + "samples_ns": [ + 24045.2210295 + ] + } + }, + "variant": "SQLite_File" + }, + { + "file_bytes": null, + "operations": { + "create": { + "max_ns": 2158.5640278, + "median_ns": 2158.5640278, + "min_ns": 2158.5640278, + "samples_ns": [ + 2158.5640278 + ] + }, + "delete": { + "max_ns": 2729.3789072, + "median_ns": 2729.3789072, + "min_ns": 2729.3789072, + "samples_ns": [ + 2729.3789072 + ] + }, + "query_all": { + "max_ns": 2.2649076, + "median_ns": 2.2649076, + "min_ns": 2.2649076, + "samples_ns": [ + 2.2649076 + ] + }, + "query_by_from": { + "max_ns": 1562.742527, + "median_ns": 1562.742527, + "min_ns": 1562.742527, + "samples_ns": [ + 1562.742527 + ] + }, + "query_by_from_to": { + "max_ns": 1260.4842603, + "median_ns": 1260.4842603, + "min_ns": 1260.4842603, + "samples_ns": [ + 1260.4842603 + ] + }, + "query_by_id": { + "max_ns": 35.0061591, + "median_ns": 35.0061591, + "min_ns": 35.0061591, + "samples_ns": [ + 35.0061591 + ] + }, + "query_by_to": { + "max_ns": 1612.5049059, + "median_ns": 1612.5049059, + "min_ns": 1612.5049059, + "samples_ns": [ + 1612.5049059 + ] + }, + "update": { + "max_ns": 6553.1346996, + "median_ns": 6553.1346996, + "min_ns": 6553.1346996, + "samples_ns": [ + 6553.1346996 + ] + } + }, + "variant": "Doublets_United_Volatile" + }, + { + "file_bytes": 335544320, + "operations": { + "create": { + "max_ns": 2423.5460254, + "median_ns": 2423.5460254, + "min_ns": 2423.5460254, + "samples_ns": [ + 2423.5460254 + ] + }, + "delete": { + "max_ns": 2959.0282873, + "median_ns": 2959.0282873, + "min_ns": 2959.0282873, + "samples_ns": [ + 2959.0282873 + ] + }, + "query_all": { + "max_ns": 2.3646066, + "median_ns": 2.3646066, + "min_ns": 2.3646066, + "samples_ns": [ + 2.3646066 + ] + }, + "query_by_from": { + "max_ns": 1517.4072318, + "median_ns": 1517.4072318, + "min_ns": 1517.4072318, + "samples_ns": [ + 1517.4072318 + ] + }, + "query_by_from_to": { + "max_ns": 1269.4602683, + "median_ns": 1269.4602683, + "min_ns": 1269.4602683, + "samples_ns": [ + 1269.4602683 + ] + }, + "query_by_id": { + "max_ns": 32.6322202, + "median_ns": 32.6322202, + "min_ns": 32.6322202, + "samples_ns": [ + 32.6322202 + ] + }, + "query_by_to": { + "max_ns": 1563.8500517, + "median_ns": 1563.8500517, + "min_ns": 1563.8500517, + "samples_ns": [ + 1563.8500517 + ] + }, + "update": { + "max_ns": 6913.0244989, + "median_ns": 6913.0244989, + "min_ns": 6913.0244989, + "samples_ns": [ + 6913.0244989 + ] + } + }, + "variant": "Doublets_United_NonVolatile" + }, + { + "file_bytes": null, + "operations": { + "create": { + "max_ns": 316.9267956, + "median_ns": 316.9267956, + "min_ns": 316.9267956, + "samples_ns": [ + 316.9267956 + ] + }, + "delete": { + "max_ns": 2638.2215362, + "median_ns": 2638.2215362, + "min_ns": 2638.2215362, + "samples_ns": [ + 2638.2215362 + ] + }, + "query_all": { + "max_ns": 3.8761177, + "median_ns": 3.8761177, + "min_ns": 3.8761177, + "samples_ns": [ + 3.8761177 + ] + }, + "query_by_from": { + "max_ns": 170.2740969, + "median_ns": 170.2740969, + "min_ns": 170.2740969, + "samples_ns": [ + 170.2740969 + ] + }, + "query_by_from_to": { + "max_ns": 240.1459662, + "median_ns": 240.1459662, + "min_ns": 240.1459662, + "samples_ns": [ + 240.1459662 + ] + }, + "query_by_id": { + "max_ns": 30.0875967, + "median_ns": 30.0875967, + "min_ns": 30.0875967, + "samples_ns": [ + 30.0875967 + ] + }, + "query_by_to": { + "max_ns": 168.7455574, + "median_ns": 168.7455574, + "min_ns": 168.7455574, + "samples_ns": [ + 168.7455574 + ] + }, + "update": { + "max_ns": 751.8905032, + "median_ns": 751.8905032, + "min_ns": 751.8905032, + "samples_ns": [ + 751.8905032 + ] + } + }, + "variant": "Doublets_Split_Volatile" + }, + { + "file_bytes": 419430400, + "operations": { + "create": { + "max_ns": 467.0320689, + "median_ns": 467.0320689, + "min_ns": 467.0320689, + "samples_ns": [ + 467.0320689 + ] + }, + "delete": { + "max_ns": 2916.4657281, + "median_ns": 2916.4657281, + "min_ns": 2916.4657281, + "samples_ns": [ + 2916.4657281 + ] + }, + "query_all": { + "max_ns": 3.9391141, + "median_ns": 3.9391141, + "min_ns": 3.9391141, + "samples_ns": [ + 3.9391141 + ] + }, + "query_by_from": { + "max_ns": 180.6547247, + "median_ns": 180.6547247, + "min_ns": 180.6547247, + "samples_ns": [ + 180.6547247 + ] + }, + "query_by_from_to": { + "max_ns": 251.0229042, + "median_ns": 251.0229042, + "min_ns": 251.0229042, + "samples_ns": [ + 251.0229042 + ] + }, + "query_by_id": { + "max_ns": 34.7244875, + "median_ns": 34.7244875, + "min_ns": 34.7244875, + "samples_ns": [ + 34.7244875 + ] + }, + "query_by_to": { + "max_ns": 187.4905362, + "median_ns": 187.4905362, + "min_ns": 187.4905362, + "samples_ns": [ + 187.4905362 + ] + }, + "update": { + "max_ns": 780.8327303, + "median_ns": 780.8327303, + "min_ns": 780.8327303, + "samples_ns": [ + 780.8327303 + ] + } + }, + "variant": "Doublets_Split_NonVolatile" + } + ], + "size": 10000000, + "sqlite_version": "3.53.2", + "warm_up_size": 10000, + "run_url": "https://github.com/linksplatform/Comparisons.SQLiteVSDoublets/actions/runs/37226110636", + "date": "2026-10-04", + "machine": "Ubuntu 24.04.5 LTS, AMD EPYC 7763 64-Core Processor, 4 cores, 16 GiB" +} diff --git a/docs/benchmarks/results/links-rust-64-100000.json b/docs/benchmarks/results/links-rust-64-100000.json new file mode 100644 index 0000000..1bde776 --- /dev/null +++ b/docs/benchmarks/results/links-rust-64-100000.json @@ -0,0 +1,866 @@ +{ + "bits": 64, + "category": "links", + "language": "Rust", + "repetitions": 10, + "results": [ + { + "file_bytes": null, + "operations": { + "create": { + "max_ns": 1797.02248, + "median_ns": 1689.3445299999998, + "min_ns": 1683.68388, + "samples_ns": [ + 1727.58114, + 1683.68388, + 1797.02248, + 1691.79968, + 1690.0989, + 1688.59016, + 1717.92767, + 1687.42538, + 1683.85862, + 1687.58246 + ] + }, + "delete": { + "max_ns": 2037.28778, + "median_ns": 2014.415635, + "min_ns": 2006.11408, + "samples_ns": [ + 2023.13494, + 2009.52723, + 2015.18764, + 2007.18293, + 2015.71773, + 2019.43875, + 2013.64363, + 2006.11408, + 2037.28778, + 2010.98547 + ] + }, + "query_all": { + "max_ns": 127.80762, + "median_ns": 126.380705, + "min_ns": 125.94242, + "samples_ns": [ + 126.47272, + 126.29279, + 127.80762, + 126.85284, + 125.95205, + 125.94242, + 127.68224, + 126.00496, + 126.46862, + 126.22857 + ] + }, + "query_by_from": { + "max_ns": 762.12659, + "median_ns": 752.324725, + "min_ns": 746.66281, + "samples_ns": [ + 746.66281, + 748.48002, + 755.5162, + 756.72387, + 754.39019, + 762.12659, + 748.73696, + 750.25926, + 747.24794, + 754.73402 + ] + }, + "query_by_from_to": { + "max_ns": 730.62475, + "median_ns": 721.304545, + "min_ns": 711.74294, + "samples_ns": [ + 727.05046, + 723.70459, + 722.71594, + 730.62475, + 717.14339, + 717.17895, + 717.89648, + 711.74294, + 722.58208, + 720.02701 + ] + }, + "query_by_id": { + "max_ns": 616.60858, + "median_ns": 615.886025, + "min_ns": 605.39276, + "samples_ns": [ + 615.72142, + 616.30621, + 611.74676, + 616.60858, + 616.05063, + 605.39276, + 615.62813, + 612.41991, + 616.3801, + 616.06532 + ] + }, + "query_by_to": { + "max_ns": 766.59937, + "median_ns": 742.6540299999999, + "min_ns": 738.05318, + "samples_ns": [ + 742.73505, + 743.27246, + 745.94767, + 746.58145, + 739.68042, + 766.59937, + 738.05318, + 740.60494, + 742.57301, + 741.24453 + ] + }, + "update": { + "max_ns": 3259.16346, + "median_ns": 3115.520205, + "min_ns": 3106.76503, + "samples_ns": [ + 3116.31745, + 3259.16346, + 3184.98946, + 3114.72296, + 3123.32308, + 3154.88509, + 3113.23109, + 3108.5706, + 3106.76503, + 3108.48424 + ] + } + }, + "variant": "SQLite_Memory" + }, + { + "file_bytes": 4620288, + "operations": { + "create": { + "max_ns": 2056.23475, + "median_ns": 2023.4541250000002, + "min_ns": 2006.50183, + "samples_ns": [ + 2035.87715, + 2023.00222, + 2027.4409, + 2056.23475, + 2019.40893, + 2023.47528, + 2039.56016, + 2023.43297, + 2016.25084, + 2006.50183 + ] + }, + "delete": { + "max_ns": 6876.24948, + "median_ns": 5175.44518, + "min_ns": 5129.5324, + "samples_ns": [ + 5186.72288, + 5178.55635, + 5167.21672, + 5186.87366, + 5198.13362, + 5145.34782, + 6876.24948, + 5172.33401, + 5129.5324, + 5157.35433 + ] + }, + "query_all": { + "max_ns": 131.64329, + "median_ns": 130.330645, + "min_ns": 129.82241, + "samples_ns": [ + 131.64329, + 130.12626, + 130.11909, + 130.7167, + 130.04926, + 130.53503, + 131.24356, + 130.07188, + 131.32913, + 129.82241 + ] + }, + "query_by_from": { + "max_ns": 810.57778, + "median_ns": 789.32199, + "min_ns": 785.33007, + "samples_ns": [ + 810.57778, + 791.38654, + 785.33007, + 789.32483, + 787.55509, + 787.71881, + 796.32784, + 790.6334, + 789.31915, + 787.58271 + ] + }, + "query_by_from_to": { + "max_ns": 769.91636, + "median_ns": 761.57334, + "min_ns": 756.63146, + "samples_ns": [ + 769.91636, + 763.04884, + 763.49438, + 759.94853, + 759.76558, + 756.63146, + 760.89427, + 763.40166, + 762.25241, + 758.96036 + ] + }, + "query_by_id": { + "max_ns": 644.29702, + "median_ns": 639.00142, + "min_ns": 634.4132, + "samples_ns": [ + 644.29702, + 639.5725, + 636.09174, + 642.29874, + 634.4132, + 637.68737, + 639.45933, + 640.10082, + 637.07698, + 638.54351 + ] + }, + "query_by_to": { + "max_ns": 796.64286, + "median_ns": 788.373685, + "min_ns": 782.35561, + "samples_ns": [ + 796.64286, + 785.36471, + 782.35561, + 783.02696, + 790.90285, + 787.54689, + 790.88577, + 788.22291, + 789.89463, + 788.52446 + ] + }, + "update": { + "max_ns": 10531.3629, + "median_ns": 10421.830624999999, + "min_ns": 10370.12225, + "samples_ns": [ + 10515.47653, + 10531.3629, + 10406.10218, + 10380.31929, + 10449.34434, + 10401.88766, + 10397.72951, + 10467.44207, + 10370.12225, + 10437.55907 + ] + } + }, + "variant": "SQLite_File" + }, + { + "file_bytes": null, + "operations": { + "create": { + "max_ns": 450.46415, + "median_ns": 448.536005, + "min_ns": 446.62769, + "samples_ns": [ + 446.62769, + 450.46415, + 448.06437, + 450.08384, + 450.18122, + 448.19432, + 447.51164, + 448.63865, + 448.43336, + 449.75773 + ] + }, + "delete": { + "max_ns": 448.64376, + "median_ns": 443.86563, + "min_ns": 440.41238, + "samples_ns": [ + 446.03748, + 440.41238, + 444.16549, + 442.70816, + 444.22961, + 441.39472, + 445.43696, + 442.6167, + 448.64376, + 443.56577 + ] + }, + "query_all": { + "max_ns": 1.91878, + "median_ns": 1.81178, + "min_ns": 1.78844, + "samples_ns": [ + 1.81799, + 1.89274, + 1.79735, + 1.91878, + 1.79495, + 1.79015, + 1.89354, + 1.80557, + 1.90065, + 1.78844 + ] + }, + "query_by_from": { + "max_ns": 239.5618, + "median_ns": 234.202935, + "min_ns": 232.11829, + "samples_ns": [ + 235.59949, + 233.81465, + 233.82528, + 232.99503, + 234.58059, + 235.34332, + 232.73924, + 239.5618, + 236.20893, + 232.11829 + ] + }, + "query_by_from_to": { + "max_ns": 181.47149, + "median_ns": 178.713785, + "min_ns": 177.68862, + "samples_ns": [ + 178.35315, + 178.05339, + 179.85507, + 178.68808, + 180.16154, + 179.67503, + 178.73949, + 177.68862, + 181.47149, + 177.98127 + ] + }, + "query_by_id": { + "max_ns": 3.29716, + "median_ns": 3.20083, + "min_ns": 3.18795, + "samples_ns": [ + 3.29105, + 3.18805, + 3.19898, + 3.18946, + 3.29455, + 3.18835, + 3.18795, + 3.29716, + 3.20268, + 3.2148 + ] + }, + "query_by_to": { + "max_ns": 255.40498, + "median_ns": 252.87927, + "min_ns": 252.02007, + "samples_ns": [ + 255.40498, + 253.0543, + 252.02007, + 253.01894, + 252.77227, + 252.69714, + 252.98627, + 252.48153, + 252.6792, + 253.1632 + ] + }, + "update": { + "max_ns": 1134.74206, + "median_ns": 1131.6392150000001, + "min_ns": 1125.52917, + "samples_ns": [ + 1125.52917, + 1133.76304, + 1126.33046, + 1131.81069, + 1127.04328, + 1131.80808, + 1131.47035, + 1134.74206, + 1129.87909, + 1133.62689 + ] + } + }, + "variant": "Doublets_United_Volatile" + }, + { + "file_bytes": 67108864, + "operations": { + "create": { + "max_ns": 473.56711, + "median_ns": 471.264505, + "min_ns": 468.03027, + "samples_ns": [ + 471.25338, + 468.03027, + 471.27563, + 473.56711, + 472.52857, + 471.76034, + 468.6915, + 469.61232, + 470.63402, + 472.72402 + ] + }, + "delete": { + "max_ns": 466.14834, + "median_ns": 462.049395, + "min_ns": 457.37777, + "samples_ns": [ + 457.37777, + 460.48186, + 458.25871, + 462.99576, + 458.39417, + 461.10303, + 463.32467, + 464.16675, + 466.14834, + 464.83649 + ] + }, + "query_all": { + "max_ns": 2.13439, + "median_ns": 1.8917899999999999, + "min_ns": 1.7155, + "samples_ns": [ + 1.82431, + 1.79365, + 1.91859, + 2.12126, + 2.13439, + 2.09342, + 1.86499, + 1.7155, + 2.02088, + 1.80858 + ] + }, + "query_by_from": { + "max_ns": 258.41671, + "median_ns": 254.67107, + "min_ns": 251.65018, + "samples_ns": [ + 252.00294, + 251.65018, + 254.37266, + 253.20859, + 254.96948, + 258.41671, + 256.41034, + 251.9325, + 255.92238, + 256.75043 + ] + }, + "query_by_from_to": { + "max_ns": 199.63041, + "median_ns": 197.85362, + "min_ns": 195.62522, + "samples_ns": [ + 196.38594, + 195.62522, + 196.91403, + 197.95237, + 199.06735, + 199.23677, + 199.63041, + 197.5394, + 198.82386, + 197.75487 + ] + }, + "query_by_id": { + "max_ns": 3.65774, + "median_ns": 3.5170700000000004, + "min_ns": 3.26059, + "samples_ns": [ + 3.65774, + 3.26059, + 3.52308, + 3.51106, + 3.59602, + 3.49923, + 3.49392, + 3.54082, + 3.56776, + 3.38071 + ] + }, + "query_by_to": { + "max_ns": 301.40632, + "median_ns": 268.970285, + "min_ns": 268.00242, + "samples_ns": [ + 268.01053, + 268.00242, + 277.99529, + 269.24164, + 301.40632, + 268.69893, + 268.68299, + 268.00663, + 277.50809, + 270.17721 + ] + }, + "update": { + "max_ns": 1206.89464, + "median_ns": 1193.46481, + "min_ns": 1179.03961, + "samples_ns": [ + 1188.64356, + 1183.60083, + 1203.60049, + 1182.78571, + 1191.49438, + 1179.03961, + 1200.29362, + 1206.89464, + 1204.58122, + 1195.43524 + ] + } + }, + "variant": "Doublets_United_NonVolatile" + }, + { + "file_bytes": null, + "operations": { + "create": { + "max_ns": 127.55316, + "median_ns": 114.46966499999999, + "min_ns": 102.02176, + "samples_ns": [ + 102.18106, + 118.13246, + 106.46456, + 114.58623, + 127.55316, + 114.3531, + 118.08056, + 114.64003, + 102.02176, + 112.25758 + ] + }, + "delete": { + "max_ns": 595.71693, + "median_ns": 590.92438, + "min_ns": 588.47921, + "samples_ns": [ + 591.69212, + 588.47921, + 590.57964, + 588.59363, + 594.61738, + 591.26912, + 593.27967, + 589.99104, + 595.71693, + 588.61176 + ] + }, + "query_all": { + "max_ns": 3.95489, + "median_ns": 3.7708500000000003, + "min_ns": 3.62678, + "samples_ns": [ + 3.62678, + 3.73498, + 3.66474, + 3.83617, + 3.79249, + 3.68678, + 3.74921, + 3.95489, + 3.83767, + 3.8555 + ] + }, + "query_by_from": { + "max_ns": 28.33213, + "median_ns": 27.64228, + "min_ns": 27.40559, + "samples_ns": [ + 27.45679, + 27.79542, + 27.64203, + 27.40559, + 27.80694, + 27.51499, + 27.64253, + 28.33213, + 27.43184, + 27.65346 + ] + }, + "query_by_from_to": { + "max_ns": 57.78374, + "median_ns": 56.11617, + "min_ns": 55.48484, + "samples_ns": [ + 55.50668, + 56.40887, + 55.69023, + 56.40366, + 56.13696, + 56.09538, + 57.78374, + 55.84872, + 55.48484, + 56.3757 + ] + }, + "query_by_id": { + "max_ns": 5.14361, + "median_ns": 4.123495, + "min_ns": 3.49413, + "samples_ns": [ + 3.49413, + 4.62083, + 3.56876, + 4.06218, + 4.50952, + 4.19323, + 5.14361, + 4.18481, + 3.52198, + 4.06119 + ] + }, + "query_by_to": { + "max_ns": 30.55928, + "median_ns": 29.19809, + "min_ns": 28.97432, + "samples_ns": [ + 29.23752, + 29.09164, + 30.55928, + 29.07571, + 29.68164, + 29.21497, + 29.06269, + 29.55271, + 28.97432, + 29.18121 + ] + }, + "update": { + "max_ns": 164.98607, + "median_ns": 163.657995, + "min_ns": 162.99396, + "samples_ns": [ + 163.52595, + 162.99396, + 163.48287, + 163.31606, + 164.98607, + 164.44286, + 163.56452, + 164.77007, + 164.49967, + 163.75147 + ] + } + }, + "variant": "Doublets_Split_Volatile" + }, + { + "file_bytes": 83886080, + "operations": { + "create": { + "max_ns": 128.30366, + "median_ns": 106.37604999999999, + "min_ns": 104.48246, + "samples_ns": [ + 128.30366, + 104.48246, + 105.87767, + 106.87443, + 105.7321, + 105.41751, + 104.9327, + 116.20536, + 107.19663, + 116.89585 + ] + }, + "delete": { + "max_ns": 631.75204, + "median_ns": 609.4642650000001, + "min_ns": 606.12076, + "samples_ns": [ + 608.61062, + 606.12076, + 612.79303, + 608.53658, + 609.77239, + 609.15614, + 610.7403, + 614.47637, + 631.75204, + 608.32989 + ] + }, + "query_all": { + "max_ns": 4.06259, + "median_ns": 3.74159, + "min_ns": 3.61435, + "samples_ns": [ + 4.06259, + 3.68388, + 3.70031, + 3.73097, + 3.61435, + 3.61967, + 3.75221, + 3.86873, + 3.91311, + 3.79669 + ] + }, + "query_by_from": { + "max_ns": 29.08644, + "median_ns": 28.749955, + "min_ns": 28.50213, + "samples_ns": [ + 28.70011, + 28.75892, + 28.50213, + 28.74229, + 29.08644, + 28.88315, + 28.61755, + 28.62196, + 28.75762, + 29.03914 + ] + }, + "query_by_from_to": { + "max_ns": 58.75635, + "median_ns": 57.035985, + "min_ns": 56.75551, + "samples_ns": [ + 56.75551, + 57.03704, + 57.15726, + 56.85089, + 57.03493, + 56.93074, + 56.82775, + 57.12249, + 58.75635, + 57.78895 + ] + }, + "query_by_id": { + "max_ns": 5.91835, + "median_ns": 3.9051400000000003, + "min_ns": 3.71233, + "samples_ns": [ + 5.91835, + 3.83026, + 4.20085, + 3.91541, + 3.71233, + 3.89487, + 3.83857, + 3.72957, + 4.59108, + 5.07568 + ] + }, + "query_by_to": { + "max_ns": 30.25591, + "median_ns": 29.8756, + "min_ns": 29.83263, + "samples_ns": [ + 30.18508, + 29.89815, + 29.84585, + 30.25591, + 29.83263, + 29.88001, + 29.87119, + 29.85787, + 29.85126, + 30.10944 + ] + }, + "update": { + "max_ns": 171.75411, + "median_ns": 170.98282999999998, + "min_ns": 170.39518, + "samples_ns": [ + 170.84672, + 170.92557, + 171.04009, + 171.19688, + 170.39518, + 171.25839, + 170.64003, + 171.49914, + 171.75411, + 170.65927 + ] + } + }, + "variant": "Doublets_Split_NonVolatile" + } + ], + "size": 100000, + "sqlite_version": "3.53.2", + "warm_up_size": 10000, + "run_url": "https://github.com/linksplatform/Comparisons.SQLiteVSDoublets/actions/runs/37226110636", + "date": "2026-10-04", + "machine": "Ubuntu 24.04.5 LTS, AMD EPYC 7763 64-Core Processor, 4 cores, 16 GiB" +} diff --git a/docs/benchmarks/results/links-rust-64-1000000.json b/docs/benchmarks/results/links-rust-64-1000000.json new file mode 100644 index 0000000..81433ce --- /dev/null +++ b/docs/benchmarks/results/links-rust-64-1000000.json @@ -0,0 +1,530 @@ +{ + "bits": 64, + "category": "links", + "language": "Rust", + "repetitions": 3, + "results": [ + { + "file_bytes": null, + "operations": { + "create": { + "max_ns": 2134.592224, + "median_ns": 2092.52115, + "min_ns": 2067.855214, + "samples_ns": [ + 2134.592224, + 2067.855214, + 2092.52115 + ] + }, + "delete": { + "max_ns": 3138.075152, + "median_ns": 3135.56009, + "min_ns": 3120.804217, + "samples_ns": [ + 3135.56009, + 3138.075152, + 3120.804217 + ] + }, + "query_all": { + "max_ns": 123.642769, + "median_ns": 123.536596, + "min_ns": 123.417214, + "samples_ns": [ + 123.642769, + 123.417214, + 123.536596 + ] + }, + "query_by_from": { + "max_ns": 884.256186, + "median_ns": 878.512576, + "min_ns": 874.489166, + "samples_ns": [ + 884.256186, + 878.512576, + 874.489166 + ] + }, + "query_by_from_to": { + "max_ns": 844.637359, + "median_ns": 839.181448, + "min_ns": 835.501611, + "samples_ns": [ + 839.181448, + 844.637359, + 835.501611 + ] + }, + "query_by_id": { + "max_ns": 747.345097, + "median_ns": 744.818495, + "min_ns": 738.357006, + "samples_ns": [ + 744.818495, + 738.357006, + 747.345097 + ] + }, + "query_by_to": { + "max_ns": 875.051749, + "median_ns": 857.870707, + "min_ns": 853.02436, + "samples_ns": [ + 875.051749, + 853.02436, + 857.870707 + ] + }, + "update": { + "max_ns": 4814.401499, + "median_ns": 4772.254799, + "min_ns": 4753.555118, + "samples_ns": [ + 4814.401499, + 4753.555118, + 4772.254799 + ] + } + }, + "variant": "SQLite_Memory" + }, + { + "file_bytes": 50540544, + "operations": { + "create": { + "max_ns": 5687.088707, + "median_ns": 5649.869416, + "min_ns": 5621.099585, + "samples_ns": [ + 5649.869416, + 5621.099585, + 5687.088707 + ] + }, + "delete": { + "max_ns": 10302.918121, + "median_ns": 10270.283784, + "min_ns": 10250.700275, + "samples_ns": [ + 10302.918121, + 10270.283784, + 10250.700275 + ] + }, + "query_all": { + "max_ns": 126.855184, + "median_ns": 126.659603, + "min_ns": 126.427672, + "samples_ns": [ + 126.427672, + 126.855184, + 126.659603 + ] + }, + "query_by_from": { + "max_ns": 1893.311701, + "median_ns": 1851.124685, + "min_ns": 1848.948401, + "samples_ns": [ + 1851.124685, + 1848.948401, + 1893.311701 + ] + }, + "query_by_from_to": { + "max_ns": 1837.690104, + "median_ns": 1816.609432, + "min_ns": 1813.162636, + "samples_ns": [ + 1816.609432, + 1813.162636, + 1837.690104 + ] + }, + "query_by_id": { + "max_ns": 1841.572962, + "median_ns": 1829.547366, + "min_ns": 1817.580133, + "samples_ns": [ + 1829.547366, + 1817.580133, + 1841.572962 + ] + }, + "query_by_to": { + "max_ns": 1887.100234, + "median_ns": 1843.983994, + "min_ns": 1836.396824, + "samples_ns": [ + 1843.983994, + 1836.396824, + 1887.100234 + ] + }, + "update": { + "max_ns": 18511.249567, + "median_ns": 18364.850573, + "min_ns": 18210.303455, + "samples_ns": [ + 18210.303455, + 18364.850573, + 18511.249567 + ] + } + }, + "variant": "SQLite_File" + }, + { + "file_bytes": null, + "operations": { + "create": { + "max_ns": 758.018288, + "median_ns": 749.822351, + "min_ns": 741.005471, + "samples_ns": [ + 749.822351, + 741.005471, + 758.018288 + ] + }, + "delete": { + "max_ns": 858.334722, + "median_ns": 856.274756, + "min_ns": 841.842487, + "samples_ns": [ + 856.274756, + 841.842487, + 858.334722 + ] + }, + "query_all": { + "max_ns": 3.118563, + "median_ns": 3.054493, + "min_ns": 3.041829, + "samples_ns": [ + 3.054493, + 3.118563, + 3.041829 + ] + }, + "query_by_from": { + "max_ns": 471.549564, + "median_ns": 471.000437, + "min_ns": 467.774497, + "samples_ns": [ + 467.774497, + 471.549564, + 471.000437 + ] + }, + "query_by_from_to": { + "max_ns": 386.919477, + "median_ns": 384.543893, + "min_ns": 381.608242, + "samples_ns": [ + 386.919477, + 384.543893, + 381.608242 + ] + }, + "query_by_id": { + "max_ns": 20.521027, + "median_ns": 20.162629, + "min_ns": 20.133284, + "samples_ns": [ + 20.521027, + 20.133284, + 20.162629 + ] + }, + "query_by_to": { + "max_ns": 573.58664, + "median_ns": 566.697184, + "min_ns": 551.981719, + "samples_ns": [ + 566.697184, + 551.981719, + 573.58664 + ] + }, + "update": { + "max_ns": 2552.025781, + "median_ns": 2548.269898, + "min_ns": 2547.209302, + "samples_ns": [ + 2547.209302, + 2548.269898, + 2552.025781 + ] + } + }, + "variant": "Doublets_United_Volatile" + }, + { + "file_bytes": 67108864, + "operations": { + "create": { + "max_ns": 851.527602, + "median_ns": 840.431876, + "min_ns": 838.45847, + "samples_ns": [ + 851.527602, + 838.45847, + 840.431876 + ] + }, + "delete": { + "max_ns": 1035.603996, + "median_ns": 1024.137349, + "min_ns": 1023.105933, + "samples_ns": [ + 1035.603996, + 1023.105933, + 1024.137349 + ] + }, + "query_all": { + "max_ns": 3.6795, + "median_ns": 3.50224, + "min_ns": 3.406509, + "samples_ns": [ + 3.50224, + 3.6795, + 3.406509 + ] + }, + "query_by_from": { + "max_ns": 638.340737, + "median_ns": 622.435365, + "min_ns": 609.755331, + "samples_ns": [ + 638.340737, + 609.755331, + 622.435365 + ] + }, + "query_by_from_to": { + "max_ns": 502.175319, + "median_ns": 494.63248, + "min_ns": 489.390959, + "samples_ns": [ + 494.63248, + 489.390959, + 502.175319 + ] + }, + "query_by_id": { + "max_ns": 22.73477, + "median_ns": 22.662384, + "min_ns": 22.439282, + "samples_ns": [ + 22.662384, + 22.73477, + 22.439282 + ] + }, + "query_by_to": { + "max_ns": 651.65645, + "median_ns": 642.046857, + "min_ns": 632.230947, + "samples_ns": [ + 651.65645, + 632.230947, + 642.046857 + ] + }, + "update": { + "max_ns": 2936.283972, + "median_ns": 2909.07077, + "min_ns": 2844.758121, + "samples_ns": [ + 2936.283972, + 2844.758121, + 2909.07077 + ] + } + }, + "variant": "Doublets_United_NonVolatile" + }, + { + "file_bytes": null, + "operations": { + "create": { + "max_ns": 185.361572, + "median_ns": 184.220305, + "min_ns": 183.899677, + "samples_ns": [ + 183.899677, + 184.220305, + 185.361572 + ] + }, + "delete": { + "max_ns": 1188.133456, + "median_ns": 1182.02132, + "min_ns": 1174.409351, + "samples_ns": [ + 1188.133456, + 1174.409351, + 1182.02132 + ] + }, + "query_all": { + "max_ns": 4.054411, + "median_ns": 4.049186, + "min_ns": 3.916297, + "samples_ns": [ + 4.049186, + 3.916297, + 4.054411 + ] + }, + "query_by_from": { + "max_ns": 130.337018, + "median_ns": 130.027765, + "min_ns": 128.313483, + "samples_ns": [ + 130.337018, + 128.313483, + 130.027765 + ] + }, + "query_by_from_to": { + "max_ns": 172.953495, + "median_ns": 171.402309, + "min_ns": 170.236063, + "samples_ns": [ + 171.402309, + 170.236063, + 172.953495 + ] + }, + "query_by_id": { + "max_ns": 21.640769, + "median_ns": 21.606603, + "min_ns": 21.586807, + "samples_ns": [ + 21.586807, + 21.640769, + 21.606603 + ] + }, + "query_by_to": { + "max_ns": 131.963382, + "median_ns": 131.921641, + "min_ns": 131.686677, + "samples_ns": [ + 131.963382, + 131.686677, + 131.921641 + ] + }, + "update": { + "max_ns": 489.796014, + "median_ns": 485.277229, + "min_ns": 484.463571, + "samples_ns": [ + 485.277229, + 484.463571, + 489.796014 + ] + } + }, + "variant": "Doublets_Split_Volatile" + }, + { + "file_bytes": 83886080, + "operations": { + "create": { + "max_ns": 200.889425, + "median_ns": 189.834191, + "min_ns": 187.615301, + "samples_ns": [ + 200.889425, + 187.615301, + 189.834191 + ] + }, + "delete": { + "max_ns": 1387.978254, + "median_ns": 1382.626769, + "min_ns": 1380.380667, + "samples_ns": [ + 1387.978254, + 1382.626769, + 1380.380667 + ] + }, + "query_all": { + "max_ns": 4.430908, + "median_ns": 4.398026, + "min_ns": 4.371162, + "samples_ns": [ + 4.371162, + 4.430908, + 4.398026 + ] + }, + "query_by_from": { + "max_ns": 141.857692, + "median_ns": 141.29373, + "min_ns": 140.976628, + "samples_ns": [ + 141.857692, + 140.976628, + 141.29373 + ] + }, + "query_by_from_to": { + "max_ns": 181.01779, + "median_ns": 180.623515, + "min_ns": 179.426569, + "samples_ns": [ + 179.426569, + 180.623515, + 181.01779 + ] + }, + "query_by_id": { + "max_ns": 23.884346, + "median_ns": 23.776214, + "min_ns": 23.760444, + "samples_ns": [ + 23.884346, + 23.776214, + 23.760444 + ] + }, + "query_by_to": { + "max_ns": 140.927929, + "median_ns": 140.858858, + "min_ns": 140.624581, + "samples_ns": [ + 140.624581, + 140.858858, + 140.927929 + ] + }, + "update": { + "max_ns": 533.560199, + "median_ns": 532.090306, + "min_ns": 529.063468, + "samples_ns": [ + 533.560199, + 529.063468, + 532.090306 + ] + } + }, + "variant": "Doublets_Split_NonVolatile" + } + ], + "size": 1000000, + "sqlite_version": "3.53.2", + "warm_up_size": 10000, + "run_url": "https://github.com/linksplatform/Comparisons.SQLiteVSDoublets/actions/runs/37226110636", + "date": "2026-10-04", + "machine": "Ubuntu 24.04.5 LTS, AMD EPYC 7763 64-Core Processor, 4 cores, 16 GiB" +} diff --git a/docs/benchmarks/results/links-rust-64-10000000.json b/docs/benchmarks/results/links-rust-64-10000000.json new file mode 100644 index 0000000..e1dbad0 --- /dev/null +++ b/docs/benchmarks/results/links-rust-64-10000000.json @@ -0,0 +1,434 @@ +{ + "bits": 64, + "category": "links", + "language": "Rust", + "repetitions": 1, + "results": [ + { + "file_bytes": null, + "operations": { + "create": { + "max_ns": 3338.1191261, + "median_ns": 3338.1191261, + "min_ns": 3338.1191261, + "samples_ns": [ + 3338.1191261 + ] + }, + "delete": { + "max_ns": 6304.0556299, + "median_ns": 6304.0556299, + "min_ns": 6304.0556299, + "samples_ns": [ + 6304.0556299 + ] + }, + "query_all": { + "max_ns": 105.4137958, + "median_ns": 105.4137958, + "min_ns": 105.4137958, + "samples_ns": [ + 105.4137958 + ] + }, + "query_by_from": { + "max_ns": 1571.9793292, + "median_ns": 1571.9793292, + "min_ns": 1571.9793292, + "samples_ns": [ + 1571.9793292 + ] + }, + "query_by_from_to": { + "max_ns": 1624.9165891, + "median_ns": 1624.9165891, + "min_ns": 1624.9165891, + "samples_ns": [ + 1624.9165891 + ] + }, + "query_by_id": { + "max_ns": 1618.1618189, + "median_ns": 1618.1618189, + "min_ns": 1618.1618189, + "samples_ns": [ + 1618.1618189 + ] + }, + "query_by_to": { + "max_ns": 1495.9259419, + "median_ns": 1495.9259419, + "min_ns": 1495.9259419, + "samples_ns": [ + 1495.9259419 + ] + }, + "update": { + "max_ns": 9575.2319008, + "median_ns": 9575.2319008, + "min_ns": 9575.2319008, + "samples_ns": [ + 9575.2319008 + ] + } + }, + "variant": "SQLite_Memory" + }, + { + "file_bytes": 525307904, + "operations": { + "create": { + "max_ns": 7413.8164856, + "median_ns": 7413.8164856, + "min_ns": 7413.8164856, + "samples_ns": [ + 7413.8164856 + ] + }, + "delete": { + "max_ns": 10899.9356727, + "median_ns": 10899.9356727, + "min_ns": 10899.9356727, + "samples_ns": [ + 10899.9356727 + ] + }, + "query_all": { + "max_ns": 106.3443574, + "median_ns": 106.3443574, + "min_ns": 106.3443574, + "samples_ns": [ + 106.3443574 + ] + }, + "query_by_from": { + "max_ns": 1981.1341955, + "median_ns": 1981.1341955, + "min_ns": 1981.1341955, + "samples_ns": [ + 1981.1341955 + ] + }, + "query_by_from_to": { + "max_ns": 2054.1919861, + "median_ns": 2054.1919861, + "min_ns": 2054.1919861, + "samples_ns": [ + 2054.1919861 + ] + }, + "query_by_id": { + "max_ns": 1813.2877141, + "median_ns": 1813.2877141, + "min_ns": 1813.2877141, + "samples_ns": [ + 1813.2877141 + ] + }, + "query_by_to": { + "max_ns": 1970.2004518, + "median_ns": 1970.2004518, + "min_ns": 1970.2004518, + "samples_ns": [ + 1970.2004518 + ] + }, + "update": { + "max_ns": 18315.0965935, + "median_ns": 18315.0965935, + "min_ns": 18315.0965935, + "samples_ns": [ + 18315.0965935 + ] + } + }, + "variant": "SQLite_File" + }, + { + "file_bytes": null, + "operations": { + "create": { + "max_ns": 2375.6336563, + "median_ns": 2375.6336563, + "min_ns": 2375.6336563, + "samples_ns": [ + 2375.6336563 + ] + }, + "delete": { + "max_ns": 2482.3543309, + "median_ns": 2482.3543309, + "min_ns": 2482.3543309, + "samples_ns": [ + 2482.3543309 + ] + }, + "query_all": { + "max_ns": 6.4245559, + "median_ns": 6.4245559, + "min_ns": 6.4245559, + "samples_ns": [ + 6.4245559 + ] + }, + "query_by_from": { + "max_ns": 1431.2812866, + "median_ns": 1431.2812866, + "min_ns": 1431.2812866, + "samples_ns": [ + 1431.2812866 + ] + }, + "query_by_from_to": { + "max_ns": 1300.3934958, + "median_ns": 1300.3934958, + "min_ns": 1300.3934958, + "samples_ns": [ + 1300.3934958 + ] + }, + "query_by_id": { + "max_ns": 22.3774715, + "median_ns": 22.3774715, + "min_ns": 22.3774715, + "samples_ns": [ + 22.3774715 + ] + }, + "query_by_to": { + "max_ns": 2295.8739006, + "median_ns": 2295.8739006, + "min_ns": 2295.8739006, + "samples_ns": [ + 2295.8739006 + ] + }, + "update": { + "max_ns": 6592.7679031, + "median_ns": 6592.7679031, + "min_ns": 6592.7679031, + "samples_ns": [ + 6592.7679031 + ] + } + }, + "variant": "Doublets_United_Volatile" + }, + { + "file_bytes": 671088640, + "operations": { + "create": { + "max_ns": 2724.8789537, + "median_ns": 2724.8789537, + "min_ns": 2724.8789537, + "samples_ns": [ + 2724.8789537 + ] + }, + "delete": { + "max_ns": 3358.1414529, + "median_ns": 3358.1414529, + "min_ns": 3358.1414529, + "samples_ns": [ + 3358.1414529 + ] + }, + "query_all": { + "max_ns": 6.3105853, + "median_ns": 6.3105853, + "min_ns": 6.3105853, + "samples_ns": [ + 6.3105853 + ] + }, + "query_by_from": { + "max_ns": 1669.5411108, + "median_ns": 1669.5411108, + "min_ns": 1669.5411108, + "samples_ns": [ + 1669.5411108 + ] + }, + "query_by_from_to": { + "max_ns": 1424.8444509, + "median_ns": 1424.8444509, + "min_ns": 1424.8444509, + "samples_ns": [ + 1424.8444509 + ] + }, + "query_by_id": { + "max_ns": 22.3903295, + "median_ns": 22.3903295, + "min_ns": 22.3903295, + "samples_ns": [ + 22.3903295 + ] + }, + "query_by_to": { + "max_ns": 1650.1873216, + "median_ns": 1650.1873216, + "min_ns": 1650.1873216, + "samples_ns": [ + 1650.1873216 + ] + }, + "update": { + "max_ns": 7754.6537424, + "median_ns": 7754.6537424, + "min_ns": 7754.6537424, + "samples_ns": [ + 7754.6537424 + ] + } + }, + "variant": "Doublets_United_NonVolatile" + }, + { + "file_bytes": null, + "operations": { + "create": { + "max_ns": 363.2776376, + "median_ns": 363.2776376, + "min_ns": 363.2776376, + "samples_ns": [ + 363.2776376 + ] + }, + "delete": { + "max_ns": 3049.3696606, + "median_ns": 3049.3696606, + "min_ns": 3049.3696606, + "samples_ns": [ + 3049.3696606 + ] + }, + "query_all": { + "max_ns": 6.8967251, + "median_ns": 6.8967251, + "min_ns": 6.8967251, + "samples_ns": [ + 6.8967251 + ] + }, + "query_by_from": { + "max_ns": 141.3931565, + "median_ns": 141.3931565, + "min_ns": 141.3931565, + "samples_ns": [ + 141.3931565 + ] + }, + "query_by_from_to": { + "max_ns": 246.3928652, + "median_ns": 246.3928652, + "min_ns": 246.3928652, + "samples_ns": [ + 246.3928652 + ] + }, + "query_by_id": { + "max_ns": 23.3561368, + "median_ns": 23.3561368, + "min_ns": 23.3561368, + "samples_ns": [ + 23.3561368 + ] + }, + "query_by_to": { + "max_ns": 148.2669908, + "median_ns": 148.2669908, + "min_ns": 148.2669908, + "samples_ns": [ + 148.2669908 + ] + }, + "update": { + "max_ns": 727.7397609, + "median_ns": 727.7397609, + "min_ns": 727.7397609, + "samples_ns": [ + 727.7397609 + ] + } + }, + "variant": "Doublets_Split_Volatile" + }, + { + "file_bytes": 838860800, + "operations": { + "create": { + "max_ns": 644.1431983, + "median_ns": 644.1431983, + "min_ns": 644.1431983, + "samples_ns": [ + 644.1431983 + ] + }, + "delete": { + "max_ns": 3620.2537132, + "median_ns": 3620.2537132, + "min_ns": 3620.2537132, + "samples_ns": [ + 3620.2537132 + ] + }, + "query_all": { + "max_ns": 7.0070397, + "median_ns": 7.0070397, + "min_ns": 7.0070397, + "samples_ns": [ + 7.0070397 + ] + }, + "query_by_from": { + "max_ns": 142.9159518, + "median_ns": 142.9159518, + "min_ns": 142.9159518, + "samples_ns": [ + 142.9159518 + ] + }, + "query_by_from_to": { + "max_ns": 239.8022085, + "median_ns": 239.8022085, + "min_ns": 239.8022085, + "samples_ns": [ + 239.8022085 + ] + }, + "query_by_id": { + "max_ns": 26.2115436, + "median_ns": 26.2115436, + "min_ns": 26.2115436, + "samples_ns": [ + 26.2115436 + ] + }, + "query_by_to": { + "max_ns": 144.8823164, + "median_ns": 144.8823164, + "min_ns": 144.8823164, + "samples_ns": [ + 144.8823164 + ] + }, + "update": { + "max_ns": 709.2838759, + "median_ns": 709.2838759, + "min_ns": 709.2838759, + "samples_ns": [ + 709.2838759 + ] + } + }, + "variant": "Doublets_Split_NonVolatile" + } + ], + "size": 10000000, + "sqlite_version": "3.53.2", + "warm_up_size": 10000, + "run_url": "https://github.com/linksplatform/Comparisons.SQLiteVSDoublets/actions/runs/37226110636", + "date": "2026-10-04", + "machine": "Ubuntu 24.04.5 LTS, INTEL(R) XEON(R) PLATINUM 8573C, 4 cores, 16 GiB" +} diff --git a/docs/benchmarks/results/objects-csharp-32-100000.json b/docs/benchmarks/results/objects-csharp-32-100000.json new file mode 100644 index 0000000..c07b1c7 --- /dev/null +++ b/docs/benchmarks/results/objects-csharp-32-100000.json @@ -0,0 +1,554 @@ +{ + "language": "C#", + "category": "objects", + "bits": 32, + "size": 100000, + "repetitions": 5, + "warm_up_size": 10000, + "sqlite_version": "3.53.3", + "results": [ + { + "variant": "SQLite_Memory", + "file_bytes": null, + "operations": { + "create": { + "median_ns": 5164.356, + "min_ns": 5115.934, + "max_ns": 5654.36, + "samples_ns": [ + 5654.36, + 5115.934, + 5142.022, + 5164.356, + 5169.92 + ] + }, + "read_all": { + "median_ns": 1513.357, + "min_ns": 1500.638, + "max_ns": 1745.565, + "samples_ns": [ + 1745.565, + 1513.357, + 1502.906, + 1500.638, + 1532.819 + ] + }, + "read_by_id": { + "median_ns": 2862.962, + "min_ns": 2809.337, + "max_ns": 2880.964, + "samples_ns": [ + 2862.962, + 2809.337, + 2859.843, + 2880.964, + 2880.931 + ] + }, + "delete": { + "median_ns": 2086.957, + "min_ns": 1985.947, + "max_ns": 2440.731, + "samples_ns": [ + 2440.731, + 1985.947, + 2070.047, + 2086.957, + 2101.478 + ] + } + } + }, + { + "variant": "SQLite_File", + "file_bytes": 82128896, + "operations": { + "create": { + "median_ns": 6403.446, + "min_ns": 6206.477, + "max_ns": 6442.949, + "samples_ns": [ + 6286.826, + 6403.446, + 6442.949, + 6420.055, + 6206.477 + ] + }, + "read_all": { + "median_ns": 1604.413, + "min_ns": 1592.614, + "max_ns": 1611.336, + "samples_ns": [ + 1600.986, + 1611.336, + 1604.413, + 1592.614, + 1606.995 + ] + }, + "read_by_id": { + "median_ns": 4031.609, + "min_ns": 4003.865, + "max_ns": 4048.399, + "samples_ns": [ + 4020.254, + 4048.399, + 4031.609, + 4003.865, + 4045.517 + ] + }, + "delete": { + "median_ns": 8138.256, + "min_ns": 8056.464, + "max_ns": 8594.112, + "samples_ns": [ + 8056.464, + 8594.112, + 8065.436, + 8178.738, + 8138.256 + ] + } + } + }, + { + "variant": "Doublets_United_Volatile_Cached", + "file_bytes": null, + "operations": { + "create": { + "median_ns": 16692.717, + "min_ns": 16584.367, + "max_ns": 16955.583, + "samples_ns": [ + 16955.583, + 16822.346, + 16692.717, + 16660.002, + 16584.367 + ] + }, + "read_all": { + "median_ns": 8596.709, + "min_ns": 8577.076, + "max_ns": 9564.312, + "samples_ns": [ + 9564.312, + 8588.889, + 8695.382, + 8596.709, + 8577.076 + ] + }, + "read_by_id": { + "median_ns": 2774.712, + "min_ns": 2763.406, + "max_ns": 2795.187, + "samples_ns": [ + 2782.729, + 2763.406, + 2772.232, + 2774.712, + 2795.187 + ] + }, + "delete": { + "median_ns": 11671.098, + "min_ns": 11509.541, + "max_ns": 11816.823, + "samples_ns": [ + 11816.823, + 11733.947, + 11671.098, + 11509.541, + 11575.538 + ] + } + } + }, + { + "variant": "Doublets_United_Volatile_Uncached", + "file_bytes": null, + "operations": { + "create": { + "median_ns": 177987.355, + "min_ns": 176299.001, + "max_ns": 179453.3, + "samples_ns": [ + 179053.331, + 179453.3, + 177987.355, + 176299.001, + 177216.998 + ] + }, + "read_all": { + "median_ns": 321808.026, + "min_ns": 312440.317, + "max_ns": 327250.427, + "samples_ns": [ + 320244.14, + 321808.026, + 325031.103, + 312440.317, + 327250.427 + ] + }, + "read_by_id": { + "median_ns": 324395.648, + "min_ns": 318286.133, + "max_ns": 326646.647, + "samples_ns": [ + 323020.233, + 326646.647, + 324395.648, + 318286.133, + 326073.407 + ] + }, + "delete": { + "median_ns": 10697.245, + "min_ns": 10565.154, + "max_ns": 10836.239, + "samples_ns": [ + 10699.396, + 10565.154, + 10697.245, + 10836.239, + 10657.491 + ] + } + } + }, + { + "variant": "Doublets_United_NonVolatile_Cached", + "file_bytes": 33554432, + "operations": { + "create": { + "median_ns": 16986.258, + "min_ns": 16893.692, + "max_ns": 17144.936, + "samples_ns": [ + 17144.936, + 16986.258, + 16988.356, + 16893.692, + 16979.876 + ] + }, + "read_all": { + "median_ns": 8526.481, + "min_ns": 8500.315, + "max_ns": 8651.097, + "samples_ns": [ + 8651.097, + 8563.205, + 8526.481, + 8500.315, + 8503.689 + ] + }, + "read_by_id": { + "median_ns": 2879.836, + "min_ns": 2847.876, + "max_ns": 2923.463, + "samples_ns": [ + 2879.836, + 2897.844, + 2923.463, + 2871.157, + 2847.876 + ] + }, + "delete": { + "median_ns": 11615.261, + "min_ns": 11539.564, + "max_ns": 11843.87, + "samples_ns": [ + 11664.06, + 11843.87, + 11615.261, + 11589.549, + 11539.564 + ] + } + } + }, + { + "variant": "Doublets_United_NonVolatile_Uncached", + "file_bytes": 33554432, + "operations": { + "create": { + "median_ns": 177791.1, + "min_ns": 177021.532, + "max_ns": 178692.315, + "samples_ns": [ + 178692.315, + 177791.1, + 177462.504, + 178013.91, + 177021.532 + ] + }, + "read_all": { + "median_ns": 329638.049, + "min_ns": 328370.098, + "max_ns": 331228.467, + "samples_ns": [ + 331228.467, + 329638.049, + 328370.098, + 328436.221, + 330383.062 + ] + }, + "read_by_id": { + "median_ns": 331242.38, + "min_ns": 328107.123, + "max_ns": 331748.221, + "samples_ns": [ + 330953.005, + 331748.221, + 331242.38, + 331393.893, + 328107.123 + ] + }, + "delete": { + "median_ns": 12384.285, + "min_ns": 12278.289, + "max_ns": 12851.472, + "samples_ns": [ + 12409.553, + 12373.068, + 12851.472, + 12278.289, + 12384.285 + ] + } + } + }, + { + "variant": "Doublets_Split_Volatile_Cached", + "file_bytes": null, + "operations": { + "create": { + "median_ns": 8933.484, + "min_ns": 8692.822, + "max_ns": 8996.138, + "samples_ns": [ + 8996.138, + 8933.484, + 8768.097, + 8692.822, + 8936.433 + ] + }, + "read_all": { + "median_ns": 9803.357, + "min_ns": 9764.222, + "max_ns": 9883.779, + "samples_ns": [ + 9764.222, + 9803.357, + 9827.993, + 9883.779, + 9780.413 + ] + }, + "read_by_id": { + "median_ns": 4172.813, + "min_ns": 4106.32, + "max_ns": 4243.214, + "samples_ns": [ + 4106.32, + 4243.214, + 4194.094, + 4172.813, + 4147.43 + ] + }, + "delete": { + "median_ns": 7872.568, + "min_ns": 7715.235, + "max_ns": 7955.873, + "samples_ns": [ + 7955.873, + 7906.187, + 7772.624, + 7715.235, + 7872.568 + ] + } + } + }, + { + "variant": "Doublets_Split_Volatile_Uncached", + "file_bytes": null, + "operations": { + "create": { + "median_ns": 112603.419, + "min_ns": 110521.57, + "max_ns": 113796, + "samples_ns": [ + 112603.419, + 113796, + 113039.623, + 110521.57, + 110896.129 + ] + }, + "read_all": { + "median_ns": 388991.381, + "min_ns": 379390.697, + "max_ns": 394798.965, + "samples_ns": [ + 394782.398, + 394798.965, + 387435.648, + 388991.381, + 379390.697 + ] + }, + "read_by_id": { + "median_ns": 391734.449, + "min_ns": 383610.841, + "max_ns": 399203.649, + "samples_ns": [ + 399203.649, + 397537.508, + 391734.449, + 389695.378, + 383610.841 + ] + }, + "delete": { + "median_ns": 7855.331, + "min_ns": 7824.184, + "max_ns": 7912.678, + "samples_ns": [ + 7912.678, + 7855.331, + 7824.184, + 7893.512, + 7842.557 + ] + } + } + }, + { + "variant": "Doublets_Split_NonVolatile_Cached", + "file_bytes": 41943040, + "operations": { + "create": { + "median_ns": 9637.353, + "min_ns": 9228.689, + "max_ns": 9733.799, + "samples_ns": [ + 9721.783, + 9228.689, + 9733.799, + 9541.018, + 9637.353 + ] + }, + "read_all": { + "median_ns": 9737.53, + "min_ns": 9621.628, + "max_ns": 9794.581, + "samples_ns": [ + 9794.581, + 9621.628, + 9737.53, + 9761.741, + 9674.524 + ] + }, + "read_by_id": { + "median_ns": 4260.379, + "min_ns": 4038.147, + "max_ns": 4320.996, + "samples_ns": [ + 4320.996, + 4130.338, + 4265.769, + 4260.379, + 4038.147 + ] + }, + "delete": { + "median_ns": 8286.394, + "min_ns": 7645.132, + "max_ns": 8308.757, + "samples_ns": [ + 8308.757, + 8286.394, + 8066.64, + 8302.493, + 7645.132 + ] + } + } + }, + { + "variant": "Doublets_Split_NonVolatile_Uncached", + "file_bytes": 41943040, + "operations": { + "create": { + "median_ns": 110080.749, + "min_ns": 108577.333, + "max_ns": 111955.191, + "samples_ns": [ + 111955.191, + 110384.1, + 110080.749, + 108577.333, + 109914.762 + ] + }, + "read_all": { + "median_ns": 378686.469, + "min_ns": 373951.736, + "max_ns": 387893.517, + "samples_ns": [ + 387893.517, + 378945.979, + 378686.469, + 373951.736, + 376565.843 + ] + }, + "read_by_id": { + "median_ns": 381226.523, + "min_ns": 379399.914, + "max_ns": 387579.326, + "samples_ns": [ + 387579.326, + 383451.7, + 381226.523, + 379771.304, + 379399.914 + ] + }, + "delete": { + "median_ns": 8031.268, + "min_ns": 7865.394, + "max_ns": 8551.216, + "samples_ns": [ + 8209.659, + 8031.268, + 7888.131, + 8551.216, + 7865.394 + ] + } + } + } + ], + "run_url": "https://github.com/linksplatform/Comparisons.SQLiteVSDoublets/actions/runs/37226110636", + "date": "2026-10-04", + "machine": "Ubuntu 24.04.5 LTS, AMD EPYC 7763 64-Core Processor, 4 cores, 16 GiB" +} diff --git a/docs/benchmarks/results/objects-csharp-32-1000000.json b/docs/benchmarks/results/objects-csharp-32-1000000.json new file mode 100644 index 0000000..db01f09 --- /dev/null +++ b/docs/benchmarks/results/objects-csharp-32-1000000.json @@ -0,0 +1,394 @@ +{ + "language": "C#", + "category": "objects", + "bits": 32, + "size": 1000000, + "repetitions": 1, + "warm_up_size": 10000, + "sqlite_version": "3.53.3", + "results": [ + { + "variant": "SQLite_Memory", + "file_bytes": null, + "operations": { + "create": { + "median_ns": 4167.7808, + "min_ns": 4167.7808, + "max_ns": 4167.7808, + "samples_ns": [ + 4167.7808 + ] + }, + "read_all": { + "median_ns": 1241.0636, + "min_ns": 1241.0636, + "max_ns": 1241.0636, + "samples_ns": [ + 1241.0636 + ] + }, + "read_by_id": { + "median_ns": 3025.8808, + "min_ns": 3025.8808, + "max_ns": 3025.8808, + "samples_ns": [ + 3025.8808 + ] + }, + "delete": { + "median_ns": 2928.1188, + "min_ns": 2928.1188, + "max_ns": 2928.1188, + "samples_ns": [ + 2928.1188 + ] + } + } + }, + { + "variant": "SQLite_File", + "file_bytes": 821272576, + "operations": { + "create": { + "median_ns": 8389.331, + "min_ns": 8389.331, + "max_ns": 8389.331, + "samples_ns": [ + 8389.331 + ] + }, + "read_all": { + "median_ns": 1265.7439, + "min_ns": 1265.7439, + "max_ns": 1265.7439, + "samples_ns": [ + 1265.7439 + ] + }, + "read_by_id": { + "median_ns": 4805.9878, + "min_ns": 4805.9878, + "max_ns": 4805.9878, + "samples_ns": [ + 4805.9878 + ] + }, + "delete": { + "median_ns": 16031.18, + "min_ns": 16031.18, + "max_ns": 16031.18, + "samples_ns": [ + 16031.18 + ] + } + } + }, + { + "variant": "Doublets_United_Volatile_Cached", + "file_bytes": null, + "operations": { + "create": { + "median_ns": 16521.5617, + "min_ns": 16521.5617, + "max_ns": 16521.5617, + "samples_ns": [ + 16521.5617 + ] + }, + "read_all": { + "median_ns": 7166.8534, + "min_ns": 7166.8534, + "max_ns": 7166.8534, + "samples_ns": [ + 7166.8534 + ] + }, + "read_by_id": { + "median_ns": 3769.3382, + "min_ns": 3769.3382, + "max_ns": 3769.3382, + "samples_ns": [ + 3769.3382 + ] + }, + "delete": { + "median_ns": 13971.0175, + "min_ns": 13971.0175, + "max_ns": 13971.0175, + "samples_ns": [ + 13971.0175 + ] + } + } + }, + { + "variant": "Doublets_United_Volatile_Uncached", + "file_bytes": null, + "operations": { + "create": { + "median_ns": 191719.5739, + "min_ns": 191719.5739, + "max_ns": 191719.5739, + "samples_ns": [ + 191719.5739 + ] + }, + "read_all": { + "median_ns": 248097.5758, + "min_ns": 248097.5758, + "max_ns": 248097.5758, + "samples_ns": [ + 248097.5758 + ] + }, + "read_by_id": { + "median_ns": 250708.6931, + "min_ns": 250708.6931, + "max_ns": 250708.6931, + "samples_ns": [ + 250708.6931 + ] + }, + "delete": { + "median_ns": 14054.1124, + "min_ns": 14054.1124, + "max_ns": 14054.1124, + "samples_ns": [ + 14054.1124 + ] + } + } + }, + { + "variant": "Doublets_United_NonVolatile_Cached", + "file_bytes": 335544320, + "operations": { + "create": { + "median_ns": 23262.3388, + "min_ns": 23262.3388, + "max_ns": 23262.3388, + "samples_ns": [ + 23262.3388 + ] + }, + "read_all": { + "median_ns": 7072.4882, + "min_ns": 7072.4882, + "max_ns": 7072.4882, + "samples_ns": [ + 7072.4882 + ] + }, + "read_by_id": { + "median_ns": 4045.3628, + "min_ns": 4045.3628, + "max_ns": 4045.3628, + "samples_ns": [ + 4045.3628 + ] + }, + "delete": { + "median_ns": 14299.8063, + "min_ns": 14299.8063, + "max_ns": 14299.8063, + "samples_ns": [ + 14299.8063 + ] + } + } + }, + { + "variant": "Doublets_United_NonVolatile_Uncached", + "file_bytes": 335544320, + "operations": { + "create": { + "median_ns": 195783.8185, + "min_ns": 195783.8185, + "max_ns": 195783.8185, + "samples_ns": [ + 195783.8185 + ] + }, + "read_all": { + "median_ns": 249672.8703, + "min_ns": 249672.8703, + "max_ns": 249672.8703, + "samples_ns": [ + 249672.8703 + ] + }, + "read_by_id": { + "median_ns": 251108.0811, + "min_ns": 251108.0811, + "max_ns": 251108.0811, + "samples_ns": [ + 251108.0811 + ] + }, + "delete": { + "median_ns": 14731.8122, + "min_ns": 14731.8122, + "max_ns": 14731.8122, + "samples_ns": [ + 14731.8122 + ] + } + } + }, + { + "variant": "Doublets_Split_Volatile_Cached", + "file_bytes": null, + "operations": { + "create": { + "median_ns": 7970.9491, + "min_ns": 7970.9491, + "max_ns": 7970.9491, + "samples_ns": [ + 7970.9491 + ] + }, + "read_all": { + "median_ns": 8095.4845, + "min_ns": 8095.4845, + "max_ns": 8095.4845, + "samples_ns": [ + 8095.4845 + ] + }, + "read_by_id": { + "median_ns": 3815.1231, + "min_ns": 3815.1231, + "max_ns": 3815.1231, + "samples_ns": [ + 3815.1231 + ] + }, + "delete": { + "median_ns": 9196.3667, + "min_ns": 9196.3667, + "max_ns": 9196.3667, + "samples_ns": [ + 9196.3667 + ] + } + } + }, + { + "variant": "Doublets_Split_Volatile_Uncached", + "file_bytes": null, + "operations": { + "create": { + "median_ns": 87212.544, + "min_ns": 87212.544, + "max_ns": 87212.544, + "samples_ns": [ + 87212.544 + ] + }, + "read_all": { + "median_ns": 334520.3833, + "min_ns": 334520.3833, + "max_ns": 334520.3833, + "samples_ns": [ + 334520.3833 + ] + }, + "read_by_id": { + "median_ns": 326978.401, + "min_ns": 326978.401, + "max_ns": 326978.401, + "samples_ns": [ + 326978.401 + ] + }, + "delete": { + "median_ns": 9218.7989, + "min_ns": 9218.7989, + "max_ns": 9218.7989, + "samples_ns": [ + 9218.7989 + ] + } + } + }, + { + "variant": "Doublets_Split_NonVolatile_Cached", + "file_bytes": 419430400, + "operations": { + "create": { + "median_ns": 15630.5685, + "min_ns": 15630.5685, + "max_ns": 15630.5685, + "samples_ns": [ + 15630.5685 + ] + }, + "read_all": { + "median_ns": 8728.002, + "min_ns": 8728.002, + "max_ns": 8728.002, + "samples_ns": [ + 8728.002 + ] + }, + "read_by_id": { + "median_ns": 3896.7424, + "min_ns": 3896.7424, + "max_ns": 3896.7424, + "samples_ns": [ + 3896.7424 + ] + }, + "delete": { + "median_ns": 12850.7263, + "min_ns": 12850.7263, + "max_ns": 12850.7263, + "samples_ns": [ + 12850.7263 + ] + } + } + }, + { + "variant": "Doublets_Split_NonVolatile_Uncached", + "file_bytes": 419430400, + "operations": { + "create": { + "median_ns": 98901.1765, + "min_ns": 98901.1765, + "max_ns": 98901.1765, + "samples_ns": [ + 98901.1765 + ] + }, + "read_all": { + "median_ns": 320237.1252, + "min_ns": 320237.1252, + "max_ns": 320237.1252, + "samples_ns": [ + 320237.1252 + ] + }, + "read_by_id": { + "median_ns": 322861.8931, + "min_ns": 322861.8931, + "max_ns": 322861.8931, + "samples_ns": [ + 322861.8931 + ] + }, + "delete": { + "median_ns": 10213.0567, + "min_ns": 10213.0567, + "max_ns": 10213.0567, + "samples_ns": [ + 10213.0567 + ] + } + } + } + ], + "run_url": "https://github.com/linksplatform/Comparisons.SQLiteVSDoublets/actions/runs/37226110636", + "date": "2026-10-04", + "machine": "Ubuntu 24.04.5 LTS, AMD EPYC 9V74 80-Core Processor, 4 cores, 16 GiB" +} diff --git a/docs/benchmarks/results/objects-csharp-64-100000.json b/docs/benchmarks/results/objects-csharp-64-100000.json new file mode 100644 index 0000000..b14defa --- /dev/null +++ b/docs/benchmarks/results/objects-csharp-64-100000.json @@ -0,0 +1,554 @@ +{ + "language": "C#", + "category": "objects", + "bits": 64, + "size": 100000, + "repetitions": 5, + "warm_up_size": 10000, + "sqlite_version": "3.53.3", + "results": [ + { + "variant": "SQLite_Memory", + "file_bytes": null, + "operations": { + "create": { + "median_ns": 5059.159, + "min_ns": 5035.621, + "max_ns": 6022.244, + "samples_ns": [ + 6022.244, + 5042.193, + 5035.621, + 5080.373, + 5059.159 + ] + }, + "read_all": { + "median_ns": 1427.576, + "min_ns": 1411.864, + "max_ns": 1449.811, + "samples_ns": [ + 1427.576, + 1425.759, + 1430.633, + 1449.811, + 1411.864 + ] + }, + "read_by_id": { + "median_ns": 2728.696, + "min_ns": 2708.628, + "max_ns": 2760.55, + "samples_ns": [ + 2756.772, + 2760.55, + 2714.976, + 2728.696, + 2708.628 + ] + }, + "delete": { + "median_ns": 1899.227, + "min_ns": 1853.097, + "max_ns": 2256.03, + "samples_ns": [ + 2256.03, + 1853.097, + 1865.438, + 1914.93, + 1899.227 + ] + } + } + }, + { + "variant": "SQLite_File", + "file_bytes": 82128896, + "operations": { + "create": { + "median_ns": 6086.995, + "min_ns": 6042.175, + "max_ns": 6197.586, + "samples_ns": [ + 6086.995, + 6042.175, + 6060.713, + 6197.586, + 6181.496 + ] + }, + "read_all": { + "median_ns": 1558.353, + "min_ns": 1540.96, + "max_ns": 1568.291, + "samples_ns": [ + 1540.96, + 1551.414, + 1568.291, + 1558.353, + 1560.382 + ] + }, + "read_by_id": { + "median_ns": 3812.768, + "min_ns": 3781.9, + "max_ns": 3901.521, + "samples_ns": [ + 3795.763, + 3781.9, + 3812.768, + 3835.568, + 3901.521 + ] + }, + "delete": { + "median_ns": 7646.748, + "min_ns": 7613.701, + "max_ns": 7838.565, + "samples_ns": [ + 7650.514, + 7646.748, + 7620.872, + 7838.565, + 7613.701 + ] + } + } + }, + { + "variant": "Doublets_United_Volatile_Cached", + "file_bytes": null, + "operations": { + "create": { + "median_ns": 17817.005, + "min_ns": 17569.54, + "max_ns": 18022.643, + "samples_ns": [ + 17817.005, + 18005.467, + 17569.54, + 18022.643, + 17683.588 + ] + }, + "read_all": { + "median_ns": 9456.027, + "min_ns": 9292.049, + "max_ns": 10733.309, + "samples_ns": [ + 10733.309, + 9292.049, + 9456.027, + 9387.296, + 9484.753 + ] + }, + "read_by_id": { + "median_ns": 2827.351, + "min_ns": 2820.609, + "max_ns": 2849.162, + "samples_ns": [ + 2849.162, + 2827.351, + 2820.609, + 2826.92, + 2834.54 + ] + }, + "delete": { + "median_ns": 12719.614, + "min_ns": 12668.893, + "max_ns": 12873.296, + "samples_ns": [ + 12873.296, + 12707.648, + 12719.614, + 12854.992, + 12668.893 + ] + } + } + }, + { + "variant": "Doublets_United_Volatile_Uncached", + "file_bytes": null, + "operations": { + "create": { + "median_ns": 186702.244, + "min_ns": 184312.505, + "max_ns": 187557.248, + "samples_ns": [ + 187557.248, + 186702.244, + 187336.364, + 185181.199, + 184312.505 + ] + }, + "read_all": { + "median_ns": 346403.292, + "min_ns": 345843.795, + "max_ns": 356053.445, + "samples_ns": [ + 356053.445, + 345843.795, + 346084.992, + 346403.292, + 346441.746 + ] + }, + "read_by_id": { + "median_ns": 362227.077, + "min_ns": 359300.191, + "max_ns": 362519.902, + "samples_ns": [ + 359300.191, + 362428.313, + 362227.077, + 362519.902, + 361805.539 + ] + }, + "delete": { + "median_ns": 12635.194, + "min_ns": 12422.755, + "max_ns": 13437.116, + "samples_ns": [ + 12857.267, + 13437.116, + 12422.755, + 12458.305, + 12635.194 + ] + } + } + }, + { + "variant": "Doublets_United_NonVolatile_Cached", + "file_bytes": 67108864, + "operations": { + "create": { + "median_ns": 18523.472, + "min_ns": 18294.989, + "max_ns": 19129.302, + "samples_ns": [ + 18523.472, + 18294.989, + 19129.302, + 18429.057, + 18814.637 + ] + }, + "read_all": { + "median_ns": 9605.235, + "min_ns": 9479.671, + "max_ns": 10120.962, + "samples_ns": [ + 9479.671, + 9605.235, + 9666.094, + 9527.253, + 10120.962 + ] + }, + "read_by_id": { + "median_ns": 3086.225, + "min_ns": 3045.055, + "max_ns": 3231.849, + "samples_ns": [ + 3045.055, + 3086.225, + 3054.244, + 3128.707, + 3231.849 + ] + }, + "delete": { + "median_ns": 12939.657, + "min_ns": 12599.657, + "max_ns": 12991.601, + "samples_ns": [ + 12721.855, + 12599.657, + 12991.601, + 12953.962, + 12939.657 + ] + } + } + }, + { + "variant": "Doublets_United_NonVolatile_Uncached", + "file_bytes": 67108864, + "operations": { + "create": { + "median_ns": 183344.135, + "min_ns": 181056.054, + "max_ns": 184981.795, + "samples_ns": [ + 184981.795, + 183435.709, + 181056.054, + 183344.135, + 181515.41 + ] + }, + "read_all": { + "median_ns": 351286.201, + "min_ns": 349052.098, + "max_ns": 356462.045, + "samples_ns": [ + 356462.045, + 351298.931, + 349424.714, + 351286.201, + 349052.098 + ] + }, + "read_by_id": { + "median_ns": 355827.598, + "min_ns": 354523.491, + "max_ns": 363027.963, + "samples_ns": [ + 363027.963, + 358818.025, + 354523.491, + 355827.598, + 354762.204 + ] + }, + "delete": { + "median_ns": 13172.88, + "min_ns": 13092.998, + "max_ns": 13213.509, + "samples_ns": [ + 13213.509, + 13177.633, + 13092.998, + 13123.415, + 13172.88 + ] + } + } + }, + { + "variant": "Doublets_Split_Volatile_Cached", + "file_bytes": null, + "operations": { + "create": { + "median_ns": 8240.536, + "min_ns": 8034.707, + "max_ns": 8247.63, + "samples_ns": [ + 8240.536, + 8034.707, + 8247.63, + 8241.167, + 8205.442 + ] + }, + "read_all": { + "median_ns": 9574.725, + "min_ns": 9498.999, + "max_ns": 9609.146, + "samples_ns": [ + 9498.999, + 9552.104, + 9609.146, + 9574.725, + 9591.835 + ] + }, + "read_by_id": { + "median_ns": 4185.589, + "min_ns": 4156.766, + "max_ns": 4407.114, + "samples_ns": [ + 4180.012, + 4156.766, + 4185.589, + 4239.232, + 4407.114 + ] + }, + "delete": { + "median_ns": 7522.501, + "min_ns": 7448.998, + "max_ns": 8402.459, + "samples_ns": [ + 7448.998, + 8070.256, + 7522.501, + 7481.905, + 8402.459 + ] + } + } + }, + { + "variant": "Doublets_Split_Volatile_Uncached", + "file_bytes": null, + "operations": { + "create": { + "median_ns": 108580.172, + "min_ns": 106898.915, + "max_ns": 109056.552, + "samples_ns": [ + 108580.172, + 108593.12, + 108210.521, + 109056.552, + 106898.915 + ] + }, + "read_all": { + "median_ns": 383055.593, + "min_ns": 378063.106, + "max_ns": 386092.039, + "samples_ns": [ + 383055.593, + 386092.039, + 382968.994, + 383705.172, + 378063.106 + ] + }, + "read_by_id": { + "median_ns": 388750.103, + "min_ns": 386408.729, + "max_ns": 389793.878, + "samples_ns": [ + 389576.331, + 388750.103, + 389793.878, + 386408.729, + 386934.274 + ] + }, + "delete": { + "median_ns": 7540.719, + "min_ns": 7399.775, + "max_ns": 7712.58, + "samples_ns": [ + 7540.719, + 7712.58, + 7507.231, + 7399.775, + 7581.109 + ] + } + } + }, + { + "variant": "Doublets_Split_NonVolatile_Cached", + "file_bytes": 83886080, + "operations": { + "create": { + "median_ns": 9472.847, + "min_ns": 9411.294, + "max_ns": 9693.699, + "samples_ns": [ + 9693.699, + 9468.623, + 9472.847, + 9411.294, + 9581.97 + ] + }, + "read_all": { + "median_ns": 9555.782, + "min_ns": 9416.645, + "max_ns": 9600.508, + "samples_ns": [ + 9465.039, + 9600.508, + 9557.157, + 9416.645, + 9555.782 + ] + }, + "read_by_id": { + "median_ns": 4106.511, + "min_ns": 4077.438, + "max_ns": 4411.937, + "samples_ns": [ + 4088.74, + 4106.511, + 4077.438, + 4411.937, + 4153.564 + ] + }, + "delete": { + "median_ns": 7696.271, + "min_ns": 7658.953, + "max_ns": 7803.743, + "samples_ns": [ + 7658.953, + 7803.743, + 7660.019, + 7696.271, + 7733.082 + ] + } + } + }, + { + "variant": "Doublets_Split_NonVolatile_Uncached", + "file_bytes": 83886080, + "operations": { + "create": { + "median_ns": 108397.428, + "min_ns": 108138.84, + "max_ns": 111065.165, + "samples_ns": [ + 111065.165, + 109255.553, + 108187.473, + 108397.428, + 108138.84 + ] + }, + "read_all": { + "median_ns": 376558.471, + "min_ns": 375964.376, + "max_ns": 383890.348, + "samples_ns": [ + 383890.348, + 378524.854, + 376160.195, + 376558.471, + 375964.376 + ] + }, + "read_by_id": { + "median_ns": 384549.616, + "min_ns": 382971.933, + "max_ns": 390437.946, + "samples_ns": [ + 390437.946, + 383117.37, + 385685.724, + 384549.616, + 382971.933 + ] + }, + "delete": { + "median_ns": 8345.718, + "min_ns": 8313.004, + "max_ns": 8430.915, + "samples_ns": [ + 8361.622, + 8313.004, + 8345.718, + 8338.734, + 8430.915 + ] + } + } + } + ], + "run_url": "https://github.com/linksplatform/Comparisons.SQLiteVSDoublets/actions/runs/37226110636", + "date": "2026-10-04", + "machine": "Ubuntu 24.04.5 LTS, AMD EPYC 7763 64-Core Processor, 4 cores, 16 GiB" +} diff --git a/docs/benchmarks/results/objects-csharp-64-1000000.json b/docs/benchmarks/results/objects-csharp-64-1000000.json new file mode 100644 index 0000000..12f5e35 --- /dev/null +++ b/docs/benchmarks/results/objects-csharp-64-1000000.json @@ -0,0 +1,394 @@ +{ + "language": "C#", + "category": "objects", + "bits": 64, + "size": 1000000, + "repetitions": 1, + "warm_up_size": 10000, + "sqlite_version": "3.53.3", + "results": [ + { + "variant": "SQLite_Memory", + "file_bytes": null, + "operations": { + "create": { + "median_ns": 5710.7318, + "min_ns": 5710.7318, + "max_ns": 5710.7318, + "samples_ns": [ + 5710.7318 + ] + }, + "read_all": { + "median_ns": 1593.3678, + "min_ns": 1593.3678, + "max_ns": 1593.3678, + "samples_ns": [ + 1593.3678 + ] + }, + "read_by_id": { + "median_ns": 3412.0789, + "min_ns": 3412.0789, + "max_ns": 3412.0789, + "samples_ns": [ + 3412.0789 + ] + }, + "delete": { + "median_ns": 3179.6579, + "min_ns": 3179.6579, + "max_ns": 3179.6579, + "samples_ns": [ + 3179.6579 + ] + } + } + }, + { + "variant": "SQLite_File", + "file_bytes": 821272576, + "operations": { + "create": { + "median_ns": 7728.9604, + "min_ns": 7728.9604, + "max_ns": 7728.9604, + "samples_ns": [ + 7728.9604 + ] + }, + "read_all": { + "median_ns": 1658.5542, + "min_ns": 1658.5542, + "max_ns": 1658.5542, + "samples_ns": [ + 1658.5542 + ] + }, + "read_by_id": { + "median_ns": 5171.1161, + "min_ns": 5171.1161, + "max_ns": 5171.1161, + "samples_ns": [ + 5171.1161 + ] + }, + "delete": { + "median_ns": 11503.9471, + "min_ns": 11503.9471, + "max_ns": 11503.9471, + "samples_ns": [ + 11503.9471 + ] + } + } + }, + { + "variant": "Doublets_United_Volatile_Cached", + "file_bytes": null, + "operations": { + "create": { + "median_ns": 22289.6204, + "min_ns": 22289.6204, + "max_ns": 22289.6204, + "samples_ns": [ + 22289.6204 + ] + }, + "read_all": { + "median_ns": 9909.2138, + "min_ns": 9909.2138, + "max_ns": 9909.2138, + "samples_ns": [ + 9909.2138 + ] + }, + "read_by_id": { + "median_ns": 4254.7062, + "min_ns": 4254.7062, + "max_ns": 4254.7062, + "samples_ns": [ + 4254.7062 + ] + }, + "delete": { + "median_ns": 20275.0758, + "min_ns": 20275.0758, + "max_ns": 20275.0758, + "samples_ns": [ + 20275.0758 + ] + } + } + }, + { + "variant": "Doublets_United_Volatile_Uncached", + "file_bytes": null, + "operations": { + "create": { + "median_ns": 232407.7042, + "min_ns": 232407.7042, + "max_ns": 232407.7042, + "samples_ns": [ + 232407.7042 + ] + }, + "read_all": { + "median_ns": 318474.5316, + "min_ns": 318474.5316, + "max_ns": 318474.5316, + "samples_ns": [ + 318474.5316 + ] + }, + "read_by_id": { + "median_ns": 320271.0668, + "min_ns": 320271.0668, + "max_ns": 320271.0668, + "samples_ns": [ + 320271.0668 + ] + }, + "delete": { + "median_ns": 18230.9185, + "min_ns": 18230.9185, + "max_ns": 18230.9185, + "samples_ns": [ + 18230.9185 + ] + } + } + }, + { + "variant": "Doublets_United_NonVolatile_Cached", + "file_bytes": 671088640, + "operations": { + "create": { + "median_ns": 29192.587, + "min_ns": 29192.587, + "max_ns": 29192.587, + "samples_ns": [ + 29192.587 + ] + }, + "read_all": { + "median_ns": 9248.0303, + "min_ns": 9248.0303, + "max_ns": 9248.0303, + "samples_ns": [ + 9248.0303 + ] + }, + "read_by_id": { + "median_ns": 4039.2912, + "min_ns": 4039.2912, + "max_ns": 4039.2912, + "samples_ns": [ + 4039.2912 + ] + }, + "delete": { + "median_ns": 18298.8362, + "min_ns": 18298.8362, + "max_ns": 18298.8362, + "samples_ns": [ + 18298.8362 + ] + } + } + }, + { + "variant": "Doublets_United_NonVolatile_Uncached", + "file_bytes": 671088640, + "operations": { + "create": { + "median_ns": 235957.3719, + "min_ns": 235957.3719, + "max_ns": 235957.3719, + "samples_ns": [ + 235957.3719 + ] + }, + "read_all": { + "median_ns": 314914.4625, + "min_ns": 314914.4625, + "max_ns": 314914.4625, + "samples_ns": [ + 314914.4625 + ] + }, + "read_by_id": { + "median_ns": 319464.9593, + "min_ns": 319464.9593, + "max_ns": 319464.9593, + "samples_ns": [ + 319464.9593 + ] + }, + "delete": { + "median_ns": 20745.1737, + "min_ns": 20745.1737, + "max_ns": 20745.1737, + "samples_ns": [ + 20745.1737 + ] + } + } + }, + { + "variant": "Doublets_Split_Volatile_Cached", + "file_bytes": null, + "operations": { + "create": { + "median_ns": 10574.0393, + "min_ns": 10574.0393, + "max_ns": 10574.0393, + "samples_ns": [ + 10574.0393 + ] + }, + "read_all": { + "median_ns": 10288.7468, + "min_ns": 10288.7468, + "max_ns": 10288.7468, + "samples_ns": [ + 10288.7468 + ] + }, + "read_by_id": { + "median_ns": 4651.6166, + "min_ns": 4651.6166, + "max_ns": 4651.6166, + "samples_ns": [ + 4651.6166 + ] + }, + "delete": { + "median_ns": 11970.976, + "min_ns": 11970.976, + "max_ns": 11970.976, + "samples_ns": [ + 11970.976 + ] + } + } + }, + { + "variant": "Doublets_Split_Volatile_Uncached", + "file_bytes": null, + "operations": { + "create": { + "median_ns": 110825.004, + "min_ns": 110825.004, + "max_ns": 110825.004, + "samples_ns": [ + 110825.004 + ] + }, + "read_all": { + "median_ns": 386817.5276, + "min_ns": 386817.5276, + "max_ns": 386817.5276, + "samples_ns": [ + 386817.5276 + ] + }, + "read_by_id": { + "median_ns": 404677.2417, + "min_ns": 404677.2417, + "max_ns": 404677.2417, + "samples_ns": [ + 404677.2417 + ] + }, + "delete": { + "median_ns": 13010.8664, + "min_ns": 13010.8664, + "max_ns": 13010.8664, + "samples_ns": [ + 13010.8664 + ] + } + } + }, + { + "variant": "Doublets_Split_NonVolatile_Cached", + "file_bytes": 838860800, + "operations": { + "create": { + "median_ns": 20414.531, + "min_ns": 20414.531, + "max_ns": 20414.531, + "samples_ns": [ + 20414.531 + ] + }, + "read_all": { + "median_ns": 10957.8927, + "min_ns": 10957.8927, + "max_ns": 10957.8927, + "samples_ns": [ + 10957.8927 + ] + }, + "read_by_id": { + "median_ns": 5265.45, + "min_ns": 5265.45, + "max_ns": 5265.45, + "samples_ns": [ + 5265.45 + ] + }, + "delete": { + "median_ns": 14984.6787, + "min_ns": 14984.6787, + "max_ns": 14984.6787, + "samples_ns": [ + 14984.6787 + ] + } + } + }, + { + "variant": "Doublets_Split_NonVolatile_Uncached", + "file_bytes": 838860800, + "operations": { + "create": { + "median_ns": 132124.5373, + "min_ns": 132124.5373, + "max_ns": 132124.5373, + "samples_ns": [ + 132124.5373 + ] + }, + "read_all": { + "median_ns": 402156.0967, + "min_ns": 402156.0967, + "max_ns": 402156.0967, + "samples_ns": [ + 402156.0967 + ] + }, + "read_by_id": { + "median_ns": 406729.7946, + "min_ns": 406729.7946, + "max_ns": 406729.7946, + "samples_ns": [ + 406729.7946 + ] + }, + "delete": { + "median_ns": 16640.8482, + "min_ns": 16640.8482, + "max_ns": 16640.8482, + "samples_ns": [ + 16640.8482 + ] + } + } + } + ], + "run_url": "https://github.com/linksplatform/Comparisons.SQLiteVSDoublets/actions/runs/37226110636", + "date": "2026-10-04", + "machine": "Ubuntu 24.04.5 LTS, AMD EPYC 7763 64-Core Processor, 4 cores, 16 GiB" +} diff --git a/docs/benchmarks/results/objects-rust-32-100000.json b/docs/benchmarks/results/objects-rust-32-100000.json new file mode 100644 index 0000000..849dcc8 --- /dev/null +++ b/docs/benchmarks/results/objects-rust-32-100000.json @@ -0,0 +1,554 @@ +{ + "bits": 32, + "category": "objects", + "language": "Rust", + "repetitions": 5, + "results": [ + { + "file_bytes": null, + "operations": { + "create": { + "max_ns": 911.52297, + "median_ns": 837.90764, + "min_ns": 800.62035, + "samples_ns": [ + 911.52297, + 805.27204, + 838.23233, + 837.90764, + 800.62035 + ] + }, + "delete": { + "max_ns": 1579.69352, + "median_ns": 1487.56793, + "min_ns": 1456.13061, + "samples_ns": [ + 1579.69352, + 1487.56793, + 1522.54559, + 1476.07884, + 1456.13061 + ] + }, + "read_all": { + "max_ns": 488.92311, + "median_ns": 484.89107, + "min_ns": 453.64718, + "samples_ns": [ + 482.97567, + 488.92311, + 485.02008, + 453.64718, + 484.89107 + ] + }, + "read_by_id": { + "max_ns": 1492.78808, + "median_ns": 1371.32147, + "min_ns": 1341.65866, + "samples_ns": [ + 1492.78808, + 1368.13932, + 1467.06373, + 1341.65866, + 1371.32147 + ] + } + }, + "variant": "SQLite_Memory" + }, + { + "file_bytes": 82128896, + "operations": { + "create": { + "max_ns": 4988.10012, + "median_ns": 4960.38155, + "min_ns": 4036.02482, + "samples_ns": [ + 4036.02482, + 4966.84413, + 4906.81299, + 4960.38155, + 4988.10012 + ] + }, + "delete": { + "max_ns": 9143.02754, + "median_ns": 8031.28447, + "min_ns": 7846.2911, + "samples_ns": [ + 9143.02754, + 8033.22425, + 8031.28447, + 7915.57121, + 7846.2911 + ] + }, + "read_all": { + "max_ns": 487.50654, + "median_ns": 476.35877, + "min_ns": 468.75384, + "samples_ns": [ + 476.35877, + 468.75384, + 474.80245, + 487.50654, + 477.74242 + ] + }, + "read_by_id": { + "max_ns": 1678.46065, + "median_ns": 1630.76868, + "min_ns": 1573.97112, + "samples_ns": [ + 1573.97112, + 1641.45481, + 1678.46065, + 1630.76868, + 1608.12316 + ] + } + }, + "variant": "SQLite_File" + }, + { + "file_bytes": null, + "operations": { + "create": { + "max_ns": 4686.75297, + "median_ns": 4577.94525, + "min_ns": 4449.37048, + "samples_ns": [ + 4686.75297, + 4667.11824, + 4577.94525, + 4544.91699, + 4449.37048 + ] + }, + "delete": { + "max_ns": 4587.10423, + "median_ns": 4508.05505, + "min_ns": 4351.38041, + "samples_ns": [ + 4587.10423, + 4394.95401, + 4508.05505, + 4351.38041, + 4529.01134 + ] + }, + "read_all": { + "max_ns": 1385.73137, + "median_ns": 1351.88443, + "min_ns": 1309.49321, + "samples_ns": [ + 1309.49321, + 1385.73137, + 1350.84572, + 1351.88443, + 1381.03953 + ] + }, + "read_by_id": { + "max_ns": 2154.77493, + "median_ns": 2001.04372, + "min_ns": 1864.90353, + "samples_ns": [ + 2154.77493, + 2135.65658, + 2001.04372, + 1864.90353, + 1977.19778 + ] + } + }, + "variant": "Doublets_United_Volatile_Cached" + }, + { + "file_bytes": null, + "operations": { + "create": { + "max_ns": 95956.40582, + "median_ns": 95510.79542, + "min_ns": 94440.94455, + "samples_ns": [ + 95548.34684, + 95956.40582, + 94440.94455, + 95231.55142, + 95510.79542 + ] + }, + "delete": { + "max_ns": 4761.29471, + "median_ns": 4522.61789, + "min_ns": 4296.07101, + "samples_ns": [ + 4296.07101, + 4540.87769, + 4522.61789, + 4761.29471, + 4296.44995 + ] + }, + "read_all": { + "max_ns": 7751.4254, + "median_ns": 7589.92781, + "min_ns": 7480.52259, + "samples_ns": [ + 7589.92781, + 7645.96208, + 7494.9388, + 7480.52259, + 7751.4254 + ] + }, + "read_by_id": { + "max_ns": 8539.20017, + "median_ns": 8208.59202, + "min_ns": 8196.87552, + "samples_ns": [ + 8196.87552, + 8208.59202, + 8201.05202, + 8375.64353, + 8539.20017 + ] + } + }, + "variant": "Doublets_United_Volatile_Uncached" + }, + { + "file_bytes": 33554432, + "operations": { + "create": { + "max_ns": 4916.1975, + "median_ns": 4865.61611, + "min_ns": 4723.2643, + "samples_ns": [ + 4723.2643, + 4879.79113, + 4916.1975, + 4801.8546, + 4865.61611 + ] + }, + "delete": { + "max_ns": 5065.0435, + "median_ns": 4823.63543, + "min_ns": 4730.87728, + "samples_ns": [ + 4821.53235, + 4920.76351, + 5065.0435, + 4823.63543, + 4730.87728 + ] + }, + "read_all": { + "max_ns": 1411.47617, + "median_ns": 1409.26233, + "min_ns": 1299.92336, + "samples_ns": [ + 1409.6859, + 1411.47617, + 1409.26233, + 1299.92336, + 1361.7768 + ] + }, + "read_by_id": { + "max_ns": 2372.3388, + "median_ns": 2253.1772, + "min_ns": 2156.78795, + "samples_ns": [ + 2163.64414, + 2253.1772, + 2372.3388, + 2265.96767, + 2156.78795 + ] + } + }, + "variant": "Doublets_United_NonVolatile_Cached" + }, + { + "file_bytes": 33554432, + "operations": { + "create": { + "max_ns": 93113.34168, + "median_ns": 92675.4319, + "min_ns": 91894.86068, + "samples_ns": [ + 92675.4319, + 93113.34168, + 92846.52196, + 91894.86068, + 92533.35019 + ] + }, + "delete": { + "max_ns": 5170.5446, + "median_ns": 4858.01598, + "min_ns": 4600.90998, + "samples_ns": [ + 4600.90998, + 4858.01598, + 5068.14865, + 4848.80318, + 5170.5446 + ] + }, + "read_all": { + "max_ns": 7858.67949, + "median_ns": 7797.62279, + "min_ns": 7516.73823, + "samples_ns": [ + 7797.62279, + 7639.63999, + 7858.67949, + 7516.73823, + 7837.12517 + ] + }, + "read_by_id": { + "max_ns": 8857.08565, + "median_ns": 8540.71775, + "min_ns": 8266.45757, + "samples_ns": [ + 8540.71775, + 8266.45757, + 8806.29499, + 8527.31106, + 8857.08565 + ] + } + }, + "variant": "Doublets_United_NonVolatile_Uncached" + }, + { + "file_bytes": null, + "operations": { + "create": { + "max_ns": 2898.41866, + "median_ns": 2892.51686, + "min_ns": 2654.0611, + "samples_ns": [ + 2892.51686, + 2894.28461, + 2898.41866, + 2654.0611, + 2682.43497 + ] + }, + "delete": { + "max_ns": 2229.12895, + "median_ns": 2184.54366, + "min_ns": 2026.30939, + "samples_ns": [ + 2229.12895, + 2216.18064, + 2030.18734, + 2026.30939, + 2184.54366 + ] + }, + "read_all": { + "max_ns": 703.13466, + "median_ns": 697.89893, + "min_ns": 624.68843, + "samples_ns": [ + 703.13466, + 693.79063, + 701.81375, + 624.68843, + 697.89893 + ] + }, + "read_by_id": { + "max_ns": 612.15145, + "median_ns": 600.52634, + "min_ns": 572.93021, + "samples_ns": [ + 572.93021, + 612.15145, + 600.52634, + 577.72642, + 606.62981 + ] + } + }, + "variant": "Doublets_Split_Volatile_Cached" + }, + { + "file_bytes": null, + "operations": { + "create": { + "max_ns": 36701.67929, + "median_ns": 36321.85166, + "min_ns": 36093.14847, + "samples_ns": [ + 36701.67929, + 36093.14847, + 36384.52572, + 36321.85166, + 36105.07691 + ] + }, + "delete": { + "max_ns": 2168.22721, + "median_ns": 2114.33848, + "min_ns": 2058.91431, + "samples_ns": [ + 2114.33848, + 2168.22721, + 2136.41341, + 2058.91431, + 2097.06065 + ] + }, + "read_all": { + "max_ns": 10022.59942, + "median_ns": 9780.39692, + "min_ns": 9581.49592, + "samples_ns": [ + 10022.59942, + 9872.26274, + 9780.39692, + 9581.49592, + 9628.43896 + ] + }, + "read_by_id": { + "max_ns": 10240.91179, + "median_ns": 10093.04796, + "min_ns": 9873.3273, + "samples_ns": [ + 10178.97112, + 10093.04796, + 10240.91179, + 10003.36112, + 9873.3273 + ] + } + }, + "variant": "Doublets_Split_Volatile_Uncached" + }, + { + "file_bytes": 41943040, + "operations": { + "create": { + "max_ns": 2957.91896, + "median_ns": 2839.27405, + "min_ns": 2816.26789, + "samples_ns": [ + 2891.77915, + 2818.40883, + 2816.26789, + 2839.27405, + 2957.91896 + ] + }, + "delete": { + "max_ns": 2358.14654, + "median_ns": 2330.39022, + "min_ns": 2248.91488, + "samples_ns": [ + 2330.39022, + 2248.91488, + 2358.14654, + 2345.5925, + 2280.02311 + ] + }, + "read_all": { + "max_ns": 720.93151, + "median_ns": 641.27365, + "min_ns": 628.03326, + "samples_ns": [ + 700.17095, + 628.03326, + 641.27365, + 640.45685, + 720.93151 + ] + }, + "read_by_id": { + "max_ns": 614.65531, + "median_ns": 588.46612, + "min_ns": 555.41951, + "samples_ns": [ + 555.41951, + 595.78815, + 614.65531, + 588.46612, + 574.73767 + ] + } + }, + "variant": "Doublets_Split_NonVolatile_Cached" + }, + { + "file_bytes": 41943040, + "operations": { + "create": { + "max_ns": 35955.3576, + "median_ns": 35878.7749, + "min_ns": 35692.09072, + "samples_ns": [ + 35955.3576, + 35878.7749, + 35696.15024, + 35692.09072, + 35895.53438 + ] + }, + "delete": { + "max_ns": 2418.32847, + "median_ns": 2367.64373, + "min_ns": 2308.63072, + "samples_ns": [ + 2343.02964, + 2308.63072, + 2367.64373, + 2418.32847, + 2392.48049 + ] + }, + "read_all": { + "max_ns": 9493.34248, + "median_ns": 9328.31175, + "min_ns": 9219.46059, + "samples_ns": [ + 9239.64557, + 9328.31175, + 9357.02133, + 9493.34248, + 9219.46059 + ] + }, + "read_by_id": { + "max_ns": 10084.00633, + "median_ns": 9690.6232, + "min_ns": 9289.92372, + "samples_ns": [ + 10084.00633, + 9953.90638, + 9499.2308, + 9690.6232, + 9289.92372 + ] + } + }, + "variant": "Doublets_Split_NonVolatile_Uncached" + } + ], + "size": 100000, + "sqlite_version": "3.53.2", + "warm_up_size": 10000, + "run_url": "https://github.com/linksplatform/Comparisons.SQLiteVSDoublets/actions/runs/37226110636", + "date": "2026-10-04", + "machine": "Ubuntu 24.04.5 LTS, Intel(R) Xeon(R) 6973P-C, 4 cores, 16 GiB" +} diff --git a/docs/benchmarks/results/objects-rust-32-1000000.json b/docs/benchmarks/results/objects-rust-32-1000000.json new file mode 100644 index 0000000..16f0fb8 --- /dev/null +++ b/docs/benchmarks/results/objects-rust-32-1000000.json @@ -0,0 +1,394 @@ +{ + "bits": 32, + "category": "objects", + "language": "Rust", + "repetitions": 1, + "results": [ + { + "file_bytes": null, + "operations": { + "create": { + "max_ns": 964.684277, + "median_ns": 964.684277, + "min_ns": 964.684277, + "samples_ns": [ + 964.684277 + ] + }, + "delete": { + "max_ns": 2698.72643, + "median_ns": 2698.72643, + "min_ns": 2698.72643, + "samples_ns": [ + 2698.72643 + ] + }, + "read_all": { + "max_ns": 478.396793, + "median_ns": 478.396793, + "min_ns": 478.396793, + "samples_ns": [ + 478.396793 + ] + }, + "read_by_id": { + "max_ns": 2062.376707, + "median_ns": 2062.376707, + "min_ns": 2062.376707, + "samples_ns": [ + 2062.376707 + ] + } + }, + "variant": "SQLite_Memory" + }, + { + "file_bytes": 821272576, + "operations": { + "create": { + "max_ns": 5931.912643, + "median_ns": 5931.912643, + "min_ns": 5931.912643, + "samples_ns": [ + 5931.912643 + ] + }, + "delete": { + "max_ns": 11071.519496, + "median_ns": 11071.519496, + "min_ns": 11071.519496, + "samples_ns": [ + 11071.519496 + ] + }, + "read_all": { + "max_ns": 470.313459, + "median_ns": 470.313459, + "min_ns": 470.313459, + "samples_ns": [ + 470.313459 + ] + }, + "read_by_id": { + "max_ns": 2711.303386, + "median_ns": 2711.303386, + "min_ns": 2711.303386, + "samples_ns": [ + 2711.303386 + ] + } + }, + "variant": "SQLite_File" + }, + { + "file_bytes": null, + "operations": { + "create": { + "max_ns": 6882.484199, + "median_ns": 6882.484199, + "min_ns": 6882.484199, + "samples_ns": [ + 6882.484199 + ] + }, + "delete": { + "max_ns": 10712.556508, + "median_ns": 10712.556508, + "min_ns": 10712.556508, + "samples_ns": [ + 10712.556508 + ] + }, + "read_all": { + "max_ns": 1636.175747, + "median_ns": 1636.175747, + "min_ns": 1636.175747, + "samples_ns": [ + 1636.175747 + ] + }, + "read_by_id": { + "max_ns": 3226.446885, + "median_ns": 3226.446885, + "min_ns": 3226.446885, + "samples_ns": [ + 3226.446885 + ] + } + }, + "variant": "Doublets_United_Volatile_Cached" + }, + { + "file_bytes": null, + "operations": { + "create": { + "max_ns": 113262.967183, + "median_ns": 113262.967183, + "min_ns": 113262.967183, + "samples_ns": [ + 113262.967183 + ] + }, + "delete": { + "max_ns": 9724.87002, + "median_ns": 9724.87002, + "min_ns": 9724.87002, + "samples_ns": [ + 9724.87002 + ] + }, + "read_all": { + "max_ns": 8338.514718, + "median_ns": 8338.514718, + "min_ns": 8338.514718, + "samples_ns": [ + 8338.514718 + ] + }, + "read_by_id": { + "max_ns": 10310.954949, + "median_ns": 10310.954949, + "min_ns": 10310.954949, + "samples_ns": [ + 10310.954949 + ] + } + }, + "variant": "Doublets_United_Volatile_Uncached" + }, + { + "file_bytes": 335544320, + "operations": { + "create": { + "max_ns": 7853.001703, + "median_ns": 7853.001703, + "min_ns": 7853.001703, + "samples_ns": [ + 7853.001703 + ] + }, + "delete": { + "max_ns": 10444.395519, + "median_ns": 10444.395519, + "min_ns": 10444.395519, + "samples_ns": [ + 10444.395519 + ] + }, + "read_all": { + "max_ns": 1612.157779, + "median_ns": 1612.157779, + "min_ns": 1612.157779, + "samples_ns": [ + 1612.157779 + ] + }, + "read_by_id": { + "max_ns": 3145.401069, + "median_ns": 3145.401069, + "min_ns": 3145.401069, + "samples_ns": [ + 3145.401069 + ] + } + }, + "variant": "Doublets_United_NonVolatile_Cached" + }, + { + "file_bytes": 335544320, + "operations": { + "create": { + "max_ns": 120631.287888, + "median_ns": 120631.287888, + "min_ns": 120631.287888, + "samples_ns": [ + 120631.287888 + ] + }, + "delete": { + "max_ns": 10410.065786, + "median_ns": 10410.065786, + "min_ns": 10410.065786, + "samples_ns": [ + 10410.065786 + ] + }, + "read_all": { + "max_ns": 8457.427806, + "median_ns": 8457.427806, + "min_ns": 8457.427806, + "samples_ns": [ + 8457.427806 + ] + }, + "read_by_id": { + "max_ns": 10389.862156, + "median_ns": 10389.862156, + "min_ns": 10389.862156, + "samples_ns": [ + 10389.862156 + ] + } + }, + "variant": "Doublets_United_NonVolatile_Uncached" + }, + { + "file_bytes": null, + "operations": { + "create": { + "max_ns": 4164.152335, + "median_ns": 4164.152335, + "min_ns": 4164.152335, + "samples_ns": [ + 4164.152335 + ] + }, + "delete": { + "max_ns": 4994.361273, + "median_ns": 4994.361273, + "min_ns": 4994.361273, + "samples_ns": [ + 4994.361273 + ] + }, + "read_all": { + "max_ns": 914.210373, + "median_ns": 914.210373, + "min_ns": 914.210373, + "samples_ns": [ + 914.210373 + ] + }, + "read_by_id": { + "max_ns": 1056.08801, + "median_ns": 1056.08801, + "min_ns": 1056.08801, + "samples_ns": [ + 1056.08801 + ] + } + }, + "variant": "Doublets_Split_Volatile_Cached" + }, + { + "file_bytes": null, + "operations": { + "create": { + "max_ns": 39286.42877, + "median_ns": 39286.42877, + "min_ns": 39286.42877, + "samples_ns": [ + 39286.42877 + ] + }, + "delete": { + "max_ns": 4797.58108, + "median_ns": 4797.58108, + "min_ns": 4797.58108, + "samples_ns": [ + 4797.58108 + ] + }, + "read_all": { + "max_ns": 11196.501732, + "median_ns": 11196.501732, + "min_ns": 11196.501732, + "samples_ns": [ + 11196.501732 + ] + }, + "read_by_id": { + "max_ns": 11744.086127, + "median_ns": 11744.086127, + "min_ns": 11744.086127, + "samples_ns": [ + 11744.086127 + ] + } + }, + "variant": "Doublets_Split_Volatile_Uncached" + }, + { + "file_bytes": 419430400, + "operations": { + "create": { + "max_ns": 5515.826769, + "median_ns": 5515.826769, + "min_ns": 5515.826769, + "samples_ns": [ + 5515.826769 + ] + }, + "delete": { + "max_ns": 5701.037566, + "median_ns": 5701.037566, + "min_ns": 5701.037566, + "samples_ns": [ + 5701.037566 + ] + }, + "read_all": { + "max_ns": 913.30805, + "median_ns": 913.30805, + "min_ns": 913.30805, + "samples_ns": [ + 913.30805 + ] + }, + "read_by_id": { + "max_ns": 1118.611792, + "median_ns": 1118.611792, + "min_ns": 1118.611792, + "samples_ns": [ + 1118.611792 + ] + } + }, + "variant": "Doublets_Split_NonVolatile_Cached" + }, + { + "file_bytes": 419430400, + "operations": { + "create": { + "max_ns": 41643.527875, + "median_ns": 41643.527875, + "min_ns": 41643.527875, + "samples_ns": [ + 41643.527875 + ] + }, + "delete": { + "max_ns": 5884.635948, + "median_ns": 5884.635948, + "min_ns": 5884.635948, + "samples_ns": [ + 5884.635948 + ] + }, + "read_all": { + "max_ns": 11126.622301, + "median_ns": 11126.622301, + "min_ns": 11126.622301, + "samples_ns": [ + 11126.622301 + ] + }, + "read_by_id": { + "max_ns": 11660.612953, + "median_ns": 11660.612953, + "min_ns": 11660.612953, + "samples_ns": [ + 11660.612953 + ] + } + }, + "variant": "Doublets_Split_NonVolatile_Uncached" + } + ], + "size": 1000000, + "sqlite_version": "3.53.2", + "warm_up_size": 10000, + "run_url": "https://github.com/linksplatform/Comparisons.SQLiteVSDoublets/actions/runs/37226110636", + "date": "2026-10-04", + "machine": "Ubuntu 24.04.5 LTS, INTEL(R) XEON(R) PLATINUM 8573C, 4 cores, 16 GiB" +} diff --git a/docs/benchmarks/results/objects-rust-64-100000.json b/docs/benchmarks/results/objects-rust-64-100000.json new file mode 100644 index 0000000..cff3d36 --- /dev/null +++ b/docs/benchmarks/results/objects-rust-64-100000.json @@ -0,0 +1,554 @@ +{ + "bits": 64, + "category": "objects", + "language": "Rust", + "repetitions": 5, + "results": [ + { + "file_bytes": null, + "operations": { + "create": { + "max_ns": 1337.13201, + "median_ns": 1224.08909, + "min_ns": 1214.84961, + "samples_ns": [ + 1337.13201, + 1214.84961, + 1221.49467, + 1224.08909, + 1262.37343 + ] + }, + "delete": { + "max_ns": 1720.36888, + "median_ns": 1702.4781, + "min_ns": 1668.44679, + "samples_ns": [ + 1707.87845, + 1720.36888, + 1676.12156, + 1702.4781, + 1668.44679 + ] + }, + "read_all": { + "max_ns": 559.42295, + "median_ns": 548.0014, + "min_ns": 546.46235, + "samples_ns": [ + 547.91064, + 551.94744, + 559.42295, + 546.46235, + 548.0014 + ] + }, + "read_by_id": { + "max_ns": 1562.51431, + "median_ns": 1437.31429, + "min_ns": 1408.60457, + "samples_ns": [ + 1562.51431, + 1408.60457, + 1517.84851, + 1409.18686, + 1437.31429 + ] + } + }, + "variant": "SQLite_Memory" + }, + { + "file_bytes": 82128896, + "operations": { + "create": { + "max_ns": 2399.94766, + "median_ns": 2007.99678, + "min_ns": 1980.88294, + "samples_ns": [ + 1980.88294, + 1989.52336, + 2007.99678, + 2399.94766, + 2159.71141 + ] + }, + "delete": { + "max_ns": 7777.43368, + "median_ns": 7272.23898, + "min_ns": 6940.822, + "samples_ns": [ + 6940.822, + 7053.42799, + 7272.23898, + 7777.43368, + 7288.24567 + ] + }, + "read_all": { + "max_ns": 631.99396, + "median_ns": 625.12839, + "min_ns": 615.74016, + "samples_ns": [ + 623.01029, + 615.74016, + 629.65331, + 625.12839, + 631.99396 + ] + }, + "read_by_id": { + "max_ns": 2661.71925, + "median_ns": 2596.8424, + "min_ns": 2425.94443, + "samples_ns": [ + 2440.82881, + 2425.94443, + 2596.8424, + 2661.71925, + 2629.61405 + ] + } + }, + "variant": "SQLite_File" + }, + { + "file_bytes": null, + "operations": { + "create": { + "max_ns": 5552.16995, + "median_ns": 5170.27315, + "min_ns": 5085.56863, + "samples_ns": [ + 5552.16995, + 5443.88699, + 5170.27315, + 5163.55968, + 5085.56863 + ] + }, + "delete": { + "max_ns": 7437.09197, + "median_ns": 6120.60088, + "min_ns": 5792.20533, + "samples_ns": [ + 7437.09197, + 7150.84706, + 6120.60088, + 5792.20533, + 6041.893 + ] + }, + "read_all": { + "max_ns": 1506.88852, + "median_ns": 1461.30363, + "min_ns": 1436.43067, + "samples_ns": [ + 1506.88852, + 1502.30199, + 1441.12109, + 1461.30363, + 1436.43067 + ] + }, + "read_by_id": { + "max_ns": 2502.9497, + "median_ns": 2252.30405, + "min_ns": 2199.98993, + "samples_ns": [ + 2495.61086, + 2502.9497, + 2252.30405, + 2222.24212, + 2199.98993 + ] + } + }, + "variant": "Doublets_United_Volatile_Cached" + }, + { + "file_bytes": null, + "operations": { + "create": { + "max_ns": 96341.72146, + "median_ns": 95725.6879, + "min_ns": 95639.36147, + "samples_ns": [ + 96341.72146, + 95770.52571, + 95639.36147, + 95725.6879, + 95696.03526 + ] + }, + "delete": { + "max_ns": 5536.82061, + "median_ns": 5329.71917, + "min_ns": 5244.41305, + "samples_ns": [ + 5536.82061, + 5329.71917, + 5244.41305, + 5245.78183, + 5525.37832 + ] + }, + "read_all": { + "max_ns": 9843.00161, + "median_ns": 9794.80557, + "min_ns": 9603.30114, + "samples_ns": [ + 9836.50709, + 9766.17775, + 9843.00161, + 9794.80557, + 9603.30114 + ] + }, + "read_by_id": { + "max_ns": 10837.42037, + "median_ns": 10729.94649, + "min_ns": 10672.68985, + "samples_ns": [ + 10703.86231, + 10744.04399, + 10672.68985, + 10837.42037, + 10729.94649 + ] + } + }, + "variant": "Doublets_United_Volatile_Uncached" + }, + { + "file_bytes": 67108864, + "operations": { + "create": { + "max_ns": 5660.4845, + "median_ns": 5577.08239, + "min_ns": 5548.25659, + "samples_ns": [ + 5554.28929, + 5582.18247, + 5660.4845, + 5548.25659, + 5577.08239 + ] + }, + "delete": { + "max_ns": 5757.85609, + "median_ns": 5645.77173, + "min_ns": 5644.02669, + "samples_ns": [ + 5669.23247, + 5645.77173, + 5645.03205, + 5757.85609, + 5644.02669 + ] + }, + "read_all": { + "max_ns": 1477.1419, + "median_ns": 1449.35324, + "min_ns": 1438.21863, + "samples_ns": [ + 1447.58144, + 1449.35324, + 1438.21863, + 1477.1419, + 1466.61812 + ] + }, + "read_by_id": { + "max_ns": 2207.92731, + "median_ns": 2134.17145, + "min_ns": 2075.97535, + "samples_ns": [ + 2081.64479, + 2157.54836, + 2075.97535, + 2134.17145, + 2207.92731 + ] + } + }, + "variant": "Doublets_United_NonVolatile_Cached" + }, + { + "file_bytes": 67108864, + "operations": { + "create": { + "max_ns": 99873.69466, + "median_ns": 99726.56287, + "min_ns": 99620.50933, + "samples_ns": [ + 99873.69466, + 99622.03232, + 99853.29325, + 99620.50933, + 99726.56287 + ] + }, + "delete": { + "max_ns": 5702.66405, + "median_ns": 5684.38949, + "min_ns": 5638.95371, + "samples_ns": [ + 5684.38949, + 5659.2277, + 5686.09583, + 5702.66405, + 5638.95371 + ] + }, + "read_all": { + "max_ns": 9820.39452, + "median_ns": 9733.98838, + "min_ns": 9548.5342, + "samples_ns": [ + 9748.81056, + 9667.73151, + 9733.98838, + 9548.5342, + 9820.39452 + ] + }, + "read_by_id": { + "max_ns": 11108.42051, + "median_ns": 10965.39631, + "min_ns": 10860.87519, + "samples_ns": [ + 11108.42051, + 10860.87519, + 10965.39631, + 10956.51552, + 11095.73831 + ] + } + }, + "variant": "Doublets_United_NonVolatile_Uncached" + }, + { + "file_bytes": null, + "operations": { + "create": { + "max_ns": 3478.7518, + "median_ns": 3393.4181, + "min_ns": 3382.11859, + "samples_ns": [ + 3383.60656, + 3382.11859, + 3412.93831, + 3478.7518, + 3393.4181 + ] + }, + "delete": { + "max_ns": 2977.32566, + "median_ns": 2937.55741, + "min_ns": 2915.15414, + "samples_ns": [ + 2937.55741, + 2977.32566, + 2931.11352, + 2915.15414, + 2939.84867 + ] + }, + "read_all": { + "max_ns": 774.08412, + "median_ns": 770.59814, + "min_ns": 768.81012, + "samples_ns": [ + 769.40613, + 768.81012, + 770.59814, + 774.08412, + 772.99039 + ] + }, + "read_by_id": { + "max_ns": 619.38022, + "median_ns": 607.68506, + "min_ns": 594.92863, + "samples_ns": [ + 594.92863, + 612.19285, + 607.68506, + 605.28579, + 619.38022 + ] + } + }, + "variant": "Doublets_Split_Volatile_Cached" + }, + { + "file_bytes": null, + "operations": { + "create": { + "max_ns": 48808.81391, + "median_ns": 48116.40786, + "min_ns": 47926.8413, + "samples_ns": [ + 48273.53076, + 47982.26663, + 48116.40786, + 47926.8413, + 48808.81391 + ] + }, + "delete": { + "max_ns": 3955.49954, + "median_ns": 2971.14154, + "min_ns": 2884.79016, + "samples_ns": [ + 2903.86908, + 2884.79016, + 2971.14154, + 3081.6052, + 3955.49954 + ] + }, + "read_all": { + "max_ns": 14271.16561, + "median_ns": 14079.05286, + "min_ns": 13987.5712, + "samples_ns": [ + 13987.5712, + 14167.62167, + 14079.05286, + 14076.27745, + 14271.16561 + ] + }, + "read_by_id": { + "max_ns": 15254.57412, + "median_ns": 14453.81462, + "min_ns": 14443.58934, + "samples_ns": [ + 14445.55664, + 15210.79881, + 14453.81462, + 14443.58934, + 15254.57412 + ] + } + }, + "variant": "Doublets_Split_Volatile_Uncached" + }, + { + "file_bytes": 83886080, + "operations": { + "create": { + "max_ns": 4010.00692, + "median_ns": 3746.71375, + "min_ns": 3649.75312, + "samples_ns": [ + 3709.05332, + 4010.00692, + 3649.75312, + 3855.01888, + 3746.71375 + ] + }, + "delete": { + "max_ns": 3885.65044, + "median_ns": 3678.68694, + "min_ns": 3189.86384, + "samples_ns": [ + 3678.68694, + 3430.28938, + 3885.65044, + 3189.86384, + 3809.28342 + ] + }, + "read_all": { + "max_ns": 827.54525, + "median_ns": 800.97033, + "min_ns": 798.27191, + "samples_ns": [ + 827.54525, + 803.31629, + 798.27191, + 800.97033, + 799.53085 + ] + }, + "read_by_id": { + "max_ns": 646.05121, + "median_ns": 634.45064, + "min_ns": 615.58485, + "samples_ns": [ + 646.05121, + 624.66414, + 634.45064, + 615.58485, + 637.72784 + ] + } + }, + "variant": "Doublets_Split_NonVolatile_Cached" + }, + { + "file_bytes": 83886080, + "operations": { + "create": { + "max_ns": 48110.10559, + "median_ns": 47351.59789, + "min_ns": 47271.55816, + "samples_ns": [ + 48110.10559, + 47507.94197, + 47314.43108, + 47351.59789, + 47271.55816 + ] + }, + "delete": { + "max_ns": 3380.22401, + "median_ns": 3229.48253, + "min_ns": 3197.66218, + "samples_ns": [ + 3197.66218, + 3229.48253, + 3293.75399, + 3200.08782, + 3380.22401 + ] + }, + "read_all": { + "max_ns": 14136.04658, + "median_ns": 14066.01919, + "min_ns": 14018.9936, + "samples_ns": [ + 14136.04658, + 14132.94806, + 14018.9936, + 14046.10671, + 14066.01919 + ] + }, + "read_by_id": { + "max_ns": 14580.10363, + "median_ns": 14555.18724, + "min_ns": 14526.99868, + "samples_ns": [ + 14566.20212, + 14580.10363, + 14526.99868, + 14555.18724, + 14550.61085 + ] + } + }, + "variant": "Doublets_Split_NonVolatile_Uncached" + } + ], + "size": 100000, + "sqlite_version": "3.53.2", + "warm_up_size": 10000, + "run_url": "https://github.com/linksplatform/Comparisons.SQLiteVSDoublets/actions/runs/37226110636", + "date": "2026-10-04", + "machine": "Ubuntu 24.04.5 LTS, AMD EPYC 7763 64-Core Processor, 4 cores, 16 GiB" +} diff --git a/docs/benchmarks/results/objects-rust-64-1000000.json b/docs/benchmarks/results/objects-rust-64-1000000.json new file mode 100644 index 0000000..9d98df2 --- /dev/null +++ b/docs/benchmarks/results/objects-rust-64-1000000.json @@ -0,0 +1,394 @@ +{ + "bits": 64, + "category": "objects", + "language": "Rust", + "repetitions": 1, + "results": [ + { + "file_bytes": null, + "operations": { + "create": { + "max_ns": 970.837028, + "median_ns": 970.837028, + "min_ns": 970.837028, + "samples_ns": [ + 970.837028 + ] + }, + "delete": { + "max_ns": 2664.26132, + "median_ns": 2664.26132, + "min_ns": 2664.26132, + "samples_ns": [ + 2664.26132 + ] + }, + "read_all": { + "max_ns": 480.972972, + "median_ns": 480.972972, + "min_ns": 480.972972, + "samples_ns": [ + 480.972972 + ] + }, + "read_by_id": { + "max_ns": 2072.887928, + "median_ns": 2072.887928, + "min_ns": 2072.887928, + "samples_ns": [ + 2072.887928 + ] + } + }, + "variant": "SQLite_Memory" + }, + { + "file_bytes": 821272576, + "operations": { + "create": { + "max_ns": 5282.511572, + "median_ns": 5282.511572, + "min_ns": 5282.511572, + "samples_ns": [ + 5282.511572 + ] + }, + "delete": { + "max_ns": 10765.430153, + "median_ns": 10765.430153, + "min_ns": 10765.430153, + "samples_ns": [ + 10765.430153 + ] + }, + "read_all": { + "max_ns": 466.421156, + "median_ns": 466.421156, + "min_ns": 466.421156, + "samples_ns": [ + 466.421156 + ] + }, + "read_by_id": { + "max_ns": 2688.32349, + "median_ns": 2688.32349, + "min_ns": 2688.32349, + "samples_ns": [ + 2688.32349 + ] + } + }, + "variant": "SQLite_File" + }, + { + "file_bytes": null, + "operations": { + "create": { + "max_ns": 7421.263274, + "median_ns": 7421.263274, + "min_ns": 7421.263274, + "samples_ns": [ + 7421.263274 + ] + }, + "delete": { + "max_ns": 11016.089409, + "median_ns": 11016.089409, + "min_ns": 11016.089409, + "samples_ns": [ + 11016.089409 + ] + }, + "read_all": { + "max_ns": 1772.405507, + "median_ns": 1772.405507, + "min_ns": 1772.405507, + "samples_ns": [ + 1772.405507 + ] + }, + "read_by_id": { + "max_ns": 3266.813378, + "median_ns": 3266.813378, + "min_ns": 3266.813378, + "samples_ns": [ + 3266.813378 + ] + } + }, + "variant": "Doublets_United_Volatile_Cached" + }, + { + "file_bytes": null, + "operations": { + "create": { + "max_ns": 113976.254617, + "median_ns": 113976.254617, + "min_ns": 113976.254617, + "samples_ns": [ + 113976.254617 + ] + }, + "delete": { + "max_ns": 11830.713123, + "median_ns": 11830.713123, + "min_ns": 11830.713123, + "samples_ns": [ + 11830.713123 + ] + }, + "read_all": { + "max_ns": 8545.817957, + "median_ns": 8545.817957, + "min_ns": 8545.817957, + "samples_ns": [ + 8545.817957 + ] + }, + "read_by_id": { + "max_ns": 10754.295123, + "median_ns": 10754.295123, + "min_ns": 10754.295123, + "samples_ns": [ + 10754.295123 + ] + } + }, + "variant": "Doublets_United_Volatile_Uncached" + }, + { + "file_bytes": 671088640, + "operations": { + "create": { + "max_ns": 9166.201244, + "median_ns": 9166.201244, + "min_ns": 9166.201244, + "samples_ns": [ + 9166.201244 + ] + }, + "delete": { + "max_ns": 11834.438124, + "median_ns": 11834.438124, + "min_ns": 11834.438124, + "samples_ns": [ + 11834.438124 + ] + }, + "read_all": { + "max_ns": 1723.330465, + "median_ns": 1723.330465, + "min_ns": 1723.330465, + "samples_ns": [ + 1723.330465 + ] + }, + "read_by_id": { + "max_ns": 3448.518968, + "median_ns": 3448.518968, + "min_ns": 3448.518968, + "samples_ns": [ + 3448.518968 + ] + } + }, + "variant": "Doublets_United_NonVolatile_Cached" + }, + { + "file_bytes": 671088640, + "operations": { + "create": { + "max_ns": 123685.071768, + "median_ns": 123685.071768, + "min_ns": 123685.071768, + "samples_ns": [ + 123685.071768 + ] + }, + "delete": { + "max_ns": 12293.037176, + "median_ns": 12293.037176, + "min_ns": 12293.037176, + "samples_ns": [ + 12293.037176 + ] + }, + "read_all": { + "max_ns": 9194.031603, + "median_ns": 9194.031603, + "min_ns": 9194.031603, + "samples_ns": [ + 9194.031603 + ] + }, + "read_by_id": { + "max_ns": 11208.518321, + "median_ns": 11208.518321, + "min_ns": 11208.518321, + "samples_ns": [ + 11208.518321 + ] + } + }, + "variant": "Doublets_United_NonVolatile_Uncached" + }, + { + "file_bytes": null, + "operations": { + "create": { + "max_ns": 4833.284299, + "median_ns": 4833.284299, + "min_ns": 4833.284299, + "samples_ns": [ + 4833.284299 + ] + }, + "delete": { + "max_ns": 6821.446052, + "median_ns": 6821.446052, + "min_ns": 6821.446052, + "samples_ns": [ + 6821.446052 + ] + }, + "read_all": { + "max_ns": 918.675609, + "median_ns": 918.675609, + "min_ns": 918.675609, + "samples_ns": [ + 918.675609 + ] + }, + "read_by_id": { + "max_ns": 1194.218218, + "median_ns": 1194.218218, + "min_ns": 1194.218218, + "samples_ns": [ + 1194.218218 + ] + } + }, + "variant": "Doublets_Split_Volatile_Cached" + }, + { + "file_bytes": null, + "operations": { + "create": { + "max_ns": 43175.581272, + "median_ns": 43175.581272, + "min_ns": 43175.581272, + "samples_ns": [ + 43175.581272 + ] + }, + "delete": { + "max_ns": 5933.791656, + "median_ns": 5933.791656, + "min_ns": 5933.791656, + "samples_ns": [ + 5933.791656 + ] + }, + "read_all": { + "max_ns": 11007.523506, + "median_ns": 11007.523506, + "min_ns": 11007.523506, + "samples_ns": [ + 11007.523506 + ] + }, + "read_by_id": { + "max_ns": 11703.790013, + "median_ns": 11703.790013, + "min_ns": 11703.790013, + "samples_ns": [ + 11703.790013 + ] + } + }, + "variant": "Doublets_Split_Volatile_Uncached" + }, + { + "file_bytes": 838860800, + "operations": { + "create": { + "max_ns": 6977.919682, + "median_ns": 6977.919682, + "min_ns": 6977.919682, + "samples_ns": [ + 6977.919682 + ] + }, + "delete": { + "max_ns": 7207.518326, + "median_ns": 7207.518326, + "min_ns": 7207.518326, + "samples_ns": [ + 7207.518326 + ] + }, + "read_all": { + "max_ns": 896.012264, + "median_ns": 896.012264, + "min_ns": 896.012264, + "samples_ns": [ + 896.012264 + ] + }, + "read_by_id": { + "max_ns": 1231.106586, + "median_ns": 1231.106586, + "min_ns": 1231.106586, + "samples_ns": [ + 1231.106586 + ] + } + }, + "variant": "Doublets_Split_NonVolatile_Cached" + }, + { + "file_bytes": 838860800, + "operations": { + "create": { + "max_ns": 46354.59335, + "median_ns": 46354.59335, + "min_ns": 46354.59335, + "samples_ns": [ + 46354.59335 + ] + }, + "delete": { + "max_ns": 7575.959002, + "median_ns": 7575.959002, + "min_ns": 7575.959002, + "samples_ns": [ + 7575.959002 + ] + }, + "read_all": { + "max_ns": 11310.911876, + "median_ns": 11310.911876, + "min_ns": 11310.911876, + "samples_ns": [ + 11310.911876 + ] + }, + "read_by_id": { + "max_ns": 11950.51879, + "median_ns": 11950.51879, + "min_ns": 11950.51879, + "samples_ns": [ + 11950.51879 + ] + } + }, + "variant": "Doublets_Split_NonVolatile_Uncached" + } + ], + "size": 1000000, + "sqlite_version": "3.53.2", + "warm_up_size": 10000, + "run_url": "https://github.com/linksplatform/Comparisons.SQLiteVSDoublets/actions/runs/37226110636", + "date": "2026-10-04", + "machine": "Ubuntu 24.04.5 LTS, INTEL(R) XEON(R) PLATINUM 8573C, 4 cores, 16 GiB" +} diff --git a/experiments/csharp_objects_profile/Program.cs b/experiments/csharp_objects_profile/Program.cs new file mode 100644 index 0000000..efffaf6 --- /dev/null +++ b/experiments/csharp_objects_profile/Program.cs @@ -0,0 +1,83 @@ +// Profiles which ILinks calls make C# Doublets blog post creation grow with the number of stored posts. +// Usage: dotnet run -c Release -- [united index tree type] [internal] +// Wraps the store in a decorator that counts and times Each, Count, Create, Update and Delete calls and +// prints them per created post for the first and the last tenth of the posts. +// Results with Platform.Data.Doublets 0.18.1 and 10,000 posts: only the update that attaches a post to its +// type marker grows, so the united store's size balanced trees degenerate when thousands of links share a source. +// Mean time of that update for posts 1..1,000 -> 9,001..10,000: +// united Default 27 µs -> 322 µs +// united SizeBalancedTree 82 µs -> 369 µs +// united RecursionlessSizeBalancedTree 47 µs -> 742 µs +// united SizedAndThreadedAVLBalancedTree 22 µs -> 2.2 µs +// split 0.9 µs -> 0.2 µs +// `internal` (no external references) degenerates the same way (Default: 50 µs -> 584 µs), and plain hubs +// without the rest of the blog post links do not, so the benchmarks use the AVL trees for the united store. +using System.Diagnostics; +using Comparisons.SQLiteVSDoublets; +using Platform.Data; +using Platform.Data.Doublets; +using Platform.Data.Doublets.Memory; +using Platform.Data.Doublets.Memory.United.Generic; +using Platform.Delegates; +using Platform.Memory; + +var kind = args[0]; +var n = int.Parse(args[1]); +Console.WriteLine($"tree types: {string.Join(", ", Enum.GetNames())}"); +ILinks store = kind == "united" + ? new UnitedMemoryLinks(new HeapResizableDirectMemory(), UnitedMemoryLinks.DefaultLinksSizeStep, + new LinksConstants(args.Length <= 3 || args[3] != "internal"), args.Length > 2 ? Enum.Parse(args[2]) : IndexTreeType.Default) + : Harness.Split(new HeapResizableDirectMemory(), new HeapResizableDirectMemory(), external: true); +var profiled = new Profiled(store); +var posts = new DoubletsBlogPosts(profiled, cacheSequences: true); +var tenth = n / 10; +for (var i = 1; i <= n; i++) +{ + if (i == 1 || i == n - tenth + 1) + { + profiled.Reset(); + } + posts.Create(Dataset.BlogPost((ulong)i)); + if (i == tenth || i == n) + { + Console.WriteLine($"{kind} posts {i - tenth + 1}..{i}: {profiled.Report(tenth)}"); + } +} + +sealed class Profiled(ILinks links) : ILinks +{ + private readonly Dictionary _calls = []; + + public LinksConstants Constants => links.Constants; + + public void Reset() => _calls.Clear(); + + public string Report(int posts) => string.Join(", ", _calls.Select(call => + $"{call.Key} {(double)call.Value.Calls / posts:F1} calls {call.Value.Time.TotalNanoseconds / posts:F0} ns")); + + private TResult Timed(string name, Func work) + { + var stopwatch = Stopwatch.StartNew(); + var result = work(); + var (calls, time) = _calls.GetValueOrDefault(name); + _calls[name] = (calls + 1, time + stopwatch.Elapsed); + return result; + } + + private static string Shape(IList? restriction, ulong any) => + restriction is null ? "null" : string.Concat(restriction.Select(value => value == any ? "*" : "v")); + + public ulong Count(IList? restriction) => Timed($"Count[{Shape(restriction, Constants.Any)}]", () => links.Count(restriction)); + + public ulong Each(IList? restriction, ReadHandler? handler) => + Timed($"Each[{Shape(restriction, Constants.Any)}]", () => links.Each(restriction, handler)); + + public ulong Create(IList? substitution, WriteHandler? handler) => Timed("Create", () => links.Create(substitution, handler)); + + public ulong Update(IList? restriction, IList? substitution, WriteHandler? handler) => + Timed($"Update[{Kind(substitution![1])},{Kind(substitution[2])}]", () => links.Update(restriction, substitution, handler)); + + private string Kind(ulong address) => Constants.IsExternalReference(address) ? "raw" : address <= 7 ? $"marker{address}" : "link"; + + public ulong Delete(IList? restriction, WriteHandler? handler) => Timed("Delete", () => links.Delete(restriction, handler)); +} diff --git a/experiments/csharp_objects_profile/csharp_objects_profile.csproj b/experiments/csharp_objects_profile/csharp_objects_profile.csproj new file mode 100644 index 0000000..77a69f8 --- /dev/null +++ b/experiments/csharp_objects_profile/csharp_objects_profile.csproj @@ -0,0 +1,11 @@ + + + Exe + net10.0 + enable + enable + + + + + diff --git a/experiments/csharp_split_linked_list/Program.cs b/experiments/csharp_split_linked_list/Program.cs new file mode 100644 index 0000000..5ecddbe --- /dev/null +++ b/experiments/csharp_split_linked_list/Program.cs @@ -0,0 +1,36 @@ +// Compares the C# split store with and without the linked list of internal sources, which the Rust split store +// of doublets 0.5.0 does not use (USE_LIST = false), on the links and objects workloads of the benchmarks. +// Usage: dotnet run -c Release -- +// Results with Platform.Data.Doublets 0.18.1 (ns per operation, the warm rounds 1 and 2): the list is the default, +// and it makes updates faster while the other operations stay close, so the benchmarks keep the default. +// links 100,000, list: create 588-703, query_by_id 388-459, query_by_from 553-624, update 1093-1120 +// links 100,000, no list: create 695-855, query_by_id 388-1094, query_by_from 485-541, update 1704-1863 +// objects 20,000 (cached), list: create 21-24 µs, read_by_id 9-11 µs; no list: create 24-26 µs, read_by_id 7-8 µs +using System.Reflection; +using Comparisons.SQLiteVSDoublets; +using Platform.Data; +using Platform.Data.Doublets.Memory; +using Platform.Data.Doublets.Memory.Split.Generic; +using Platform.Memory; + +var category = args[0]; +var n = ulong.Parse(args[1]); +var defaultStore = new SplitMemoryLinks(new HeapResizableDirectMemory(), new HeapResizableDirectMemory(), + SplitMemoryLinks.DefaultLinksSizeStep, new LinksConstants(false)); +var field = typeof(SplitMemoryLinksBase).GetField("_useLinkedList", BindingFlags.NonPublic | BindingFlags.Instance)!; +Console.WriteLine($"default useLinkedList: {field.GetValue(defaultStore)}"); + +for (var round = 0; round < 3; round++) +{ + foreach (var useLinkedList in new[] { true, false }) + { + var store = new SplitMemoryLinks(new HeapResizableDirectMemory(), new HeapResizableDirectMemory(), + SplitMemoryLinks.DefaultLinksSizeStep, new LinksConstants(category == "objects"), + IndexTreeType.Default, useLinkedList); + var timings = category == "links" + ? Harness.LinksLifecycle(new DoubletsLinks(store), n, () => { }) + : Harness.ObjectsLifecycle(new DoubletsBlogPosts(store, cacheSequences: true), n, () => { }); + Console.WriteLine($"round {round}, useLinkedList {useLinkedList,-5}: " + + string.Join(", ", timings.Select(timing => $"{timing.Item1} {timing.Item2.TotalNanoseconds / n:F0}"))); + } +} diff --git a/experiments/csharp_split_linked_list/csharp_split_linked_list.csproj b/experiments/csharp_split_linked_list/csharp_split_linked_list.csproj new file mode 100644 index 0000000..77a69f8 --- /dev/null +++ b/experiments/csharp_split_linked_list/csharp_split_linked_list.csproj @@ -0,0 +1,11 @@ + + + Exe + net10.0 + enable + enable + + + + + diff --git a/experiments/csharp_tree_delete/Program.cs b/experiments/csharp_tree_delete/Program.cs new file mode 100644 index 0000000..1dd7180 --- /dev/null +++ b/experiments/csharp_tree_delete/Program.cs @@ -0,0 +1,88 @@ +// Shows why the C# benchmarks delete links with `Platform.Data.Doublets.ILinksExtensions.Delete(links, id, handler)`. +// The `links.Delete(id)` overload from Platform.Data frees the link without detaching it from the source and +// target trees, so the trees keep a stale entry that soon hides other links from `SearchOrDefault`. +// The Platform.Data.Doublets overload resets the link to (0, 0) first, which detaches it. +// Usage: dotnet run -c Release [-- --self] [-- --reset] +// --self also creates links that reference themselves; --reset deletes with the resetting overload. +// Results with Platform.Data.Doublets 0.18.1 (random stores of 3..151 links, every unreferenced link deleted): +// (no flags) united: fails at 7 links, split: fails at 13 links +// --self united: fails at 8 links, split: fails at 10 links +// --reset united and split: no failure up to 151 links +// --self --reset united and split: no failure up to 151 links +using Platform.Data; +using Platform.Data.Doublets; +using Platform.Data.Doublets.Memory.Split.Generic; +using Platform.Data.Doublets.Memory.United.Generic; +using Platform.Memory; + +var self = args.Contains("--self"); +var reset = args.Contains("--reset"); +foreach (var kind in new[] { "united", "split" }) +{ + var failure = Enumerable.Range(3, 149).Select(n => TreeDelete.Run(kind, n, self, reset)).FirstOrDefault(failure => failure is not null); + Console.WriteLine($"{kind}: {failure ?? "no failure up to 151 links"}"); +} + +static class TreeDelete +{ + public static string? Run(string kind, int n, bool self, bool reset) + { + ILinks links = kind == "united" + ? new UnitedMemoryLinks(new HeapResizableDirectMemory()) + : new SplitMemoryLinks(new HeapResizableDirectMemory(), new HeapResizableDirectMemory()); + // Every store preallocates its memory, so it must be released before the next run. + using var disposable = (IDisposable)links; + var random = new Random(n); + var created = CreateRandomLinks(links, random, n, self); + return DeleteUnreferenced(links, random, created, reset); + } + + private static List<(ulong Id, ulong Source, ulong Target)> CreateRandomLinks(ILinks links, Random random, int n, bool self) + { + // Two points, so that new (source, target) pairs exist without --self. + var created = new List<(ulong Id, ulong Source, ulong Target)> { (links.CreatePoint(), 1, 1), (links.CreatePoint(), 2, 2) }; + while (created.Count < n) + { + var next = created[^1].Id + 1; + var source = self && random.Next(4) == 0 ? next : created[random.Next(created.Count)].Id; + var target = created[random.Next(created.Count)].Id; + if (links.SearchOrDefault(source, target) == 0) + { + created.Add((links.CreateAndUpdate(source, target), source, target)); + } + } + return created; + } + + private static string? DeleteUnreferenced(ILinks links, Random random, List<(ulong Id, ulong Source, ulong Target)> created, bool reset) + { + var alive = created.ToDictionary(link => link.Id); + foreach (var victim in created.OrderBy(_ => random.Next())) + { + if (alive.Values.Any(link => link.Id != victim.Id && (link.Source == victim.Id || link.Target == victim.Id))) + { + continue; + } + Delete(links, victim.Id, reset); + alive.Remove(victim.Id); + var lost = alive.Values.FirstOrDefault(link => links.SearchOrDefault(link.Source, link.Target) != link.Id); + if (lost != default) + { + return $"{created.Count} links {string.Join(" ", created.Select(l => $"({l.Id}: {l.Source} {l.Target})"))}; after deleting {victim.Id} search({lost.Source}, {lost.Target}) = {links.SearchOrDefault(lost.Source, lost.Target)} instead of {lost.Id}"; + } + } + return null; + } + + private static void Delete(ILinks links, ulong id, bool reset) + { + if (reset) + { + links.Delete(id, handler: null); + } + else + { + links.Delete(id); + } + } +} diff --git a/experiments/csharp_tree_delete/csharp_tree_delete.csproj b/experiments/csharp_tree_delete/csharp_tree_delete.csproj new file mode 100644 index 0000000..77a69f8 --- /dev/null +++ b/experiments/csharp_tree_delete/csharp_tree_delete.csproj @@ -0,0 +1,11 @@ + + + Exe + net10.0 + enable + enable + + + + + diff --git a/experiments/csharp_warm_up_order.sh b/experiments/csharp_warm_up_order.sh new file mode 100755 index 0000000..33c7290 --- /dev/null +++ b/experiments/csharp_warm_up_order.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +# Prints the C# medians per variant twice: with a warm JIT the Volatile/NonVolatile pairs of the same +# store should agree, whatever position the variant has in the run. +# Before the warm-up ran for a second, the first variant looked up to 3x slower than its twin, e.g. links 1000: +# Doublets_United_Volatile create 6011 ns vs Doublets_United_NonVolatile 2011 ns. +set -euo pipefail +category=${1:-links} +size=${2:-1000} +dotnet build -c Release csharp/SQLiteVSDoublets > /dev/null +for run in 1 2; do + start=$SECONDS + dotnet csharp/SQLiteVSDoublets/bin/Release/net10.0/sqlite-vs-doublets.dll "$category" 64 "$size" \ + --output "/tmp/warm-up-$run.json" --directory /tmp/warm-up-storage 2> /dev/null + echo "run $run took $((SECONDS - start)) s" + python3 -c " +import json, sys +for r in json.load(open(sys.argv[1]))['results']: + print(r['variant'].ljust(38), {k: round(v['median_ns']) for k, v in r['operations'].items()})" "/tmp/warm-up-$run.json" +done diff --git a/experiments/rust_objects_ab.sh b/experiments/rust_objects_ab.sh new file mode 100755 index 0000000..125f812 --- /dev/null +++ b/experiments/rust_objects_ab.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +# Compares the Rust objects medians of the working tree with HEAD on the same machine. +set -euo pipefail +size=${1:-100000} +variants=${VARIANTS:-SQLite_Memory,Doublets_Split_Volatile_Cached,Doublets_Split_Volatile_Uncached} +run() { + cargo build --release --quiet --manifest-path rust/Cargo.toml + rust/target/release/sqlite-vs-doublets objects 64 "$size" --variants "$variants" --output "/tmp/ab-$1.json" 2> /dev/null + python3 -c " +import json, sys +for r in json.load(open(sys.argv[1]))['results']: + print(sys.argv[2], r['variant'].ljust(34), {k: round(v['median_ns']) for k, v in r['operations'].items()})" "/tmp/ab-$1.json" "$1" +} +git stash --quiet +trap 'git stash pop --quiet' EXIT +run head +git stash pop --quiet +trap - EXIT +run tree diff --git a/experiments/sizing/run.sh b/experiments/sizing/run.sh new file mode 100755 index 0000000..af22fd5 --- /dev/null +++ b/experiments/sizing/run.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +# Times one repetition of every variant to plan sizes and CI job splits. +# Usage: experiments/sizing/run.sh <32|64> [extra args] +set -euo pipefail +category=$1 bits=$2 size=$3 +shift 3 +binary="$(dirname "$0")/../../rust/target/release/sqlite-vs-doublets" +log="/tmp/sizing-$category-$bits-$size.log" +start=$(date +%s) +"$binary" "$category" "$bits" "$size" --repetitions 1 --directory "/tmp/sizing-$category-$bits-$size" \ + --output "/tmp/sizing-$category-$bits-$size.json" "$@" > "$log" 2>&1 +echo "$category $bits $size: $(( $(date +%s) - start )) s" +grep '#1' "$log" diff --git a/experiments/split_store_delete/Cargo.lock b/experiments/split_store_delete/Cargo.lock new file mode 100644 index 0000000..95de11d --- /dev/null +++ b/experiments/split_store_delete/Cargo.lock @@ -0,0 +1,274 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "allocator-api2" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c880a97d28a3681c0267bd29cff89621202715b065127cd445fa0f0fe0aa2880" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "beef" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a8241f3ebb85c056b509d4327ad0358fbbba6ffb340bf388f26350aeda225b1" + +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "doublets" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677be8ee593349204e8e7cbc2398d4b98fb95fd1386f3a7eaf286860a5788c8f" +dependencies = [ + "cfg-if", + "leak_slice", + "platform-data", + "platform-mem", + "platform-num", + "platform-trees", + "tap", + "thiserror", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi", +] + +[[package]] +name = "leak_slice" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecf3387da9fb41906394e1306ddd3cd26dd9b7177af11c19b45b364b743aed26" + +[[package]] +name = "libc" +version = "0.2.190" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "memmap2" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1219ed1b7f229ee7104d281dd01d6802fe28bb6e95d292942c4daacdeb798c0" +dependencies = [ + "libc", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "platform-data" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6782bc71345465116de96d250a36dcf49336010a2320d958d12a5d4390186c90" +dependencies = [ + "beef", + "platform-num", + "thiserror", +] + +[[package]] +name = "platform-mem" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f27cff7c92440ac926c8c91ea3151db6e52a262f602d0c157f254e422fc15b12" +dependencies = [ + "allocator-api2", + "memmap2", + "tempfile", + "thiserror", +] + +[[package]] +name = "platform-num" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c4ca8e18138b1c90ad802aff931f946a0e6bd760c35af30f1ff2489489ab54a" +dependencies = [ + "num-traits", +] + +[[package]] +name = "platform-trees" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40e25a531617fa762c8505826c930f6c1cfcc226f63dea09882b56ae0b8ed078" +dependencies = [ + "platform-num", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rustix" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "split_store_delete" +version = "0.1.0" +dependencies = [ + "doublets", +] + +[[package]] +name = "syn" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom", + "once_cell", + "rustix", + "windows-sys", +] + +[[package]] +name = "thiserror" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] diff --git a/experiments/split_store_delete/Cargo.toml b/experiments/split_store_delete/Cargo.toml new file mode 100644 index 0000000..996216e --- /dev/null +++ b/experiments/split_store_delete/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "split_store_delete" +version = "0.1.0" +edition = "2024" +publish = false + +[dependencies] +doublets = "0.5.0" + +[workspace] + diff --git a/experiments/split_store_delete/src/main.rs b/experiments/split_store_delete/src/main.rs new file mode 100644 index 0000000..d440380 --- /dev/null +++ b/experiments/split_store_delete/src/main.rs @@ -0,0 +1,81 @@ +//! Minimal reproductions of a `doublets` 0.5.0 split store bug. +//! +//! `split::Store::update_links` detaches the link and only then asks `is_virtual(new_source)`, +//! which is `is_unused`: `size_as_target == 0 && source != 0`. Right after the detach that is true +//! for the link itself, so updating a link to reference itself attaches it to the external +//! (header) trees. A later delete looks for it in its own internal trees and panics with +//! "index out of bounds" in `InternalSourcesRecursionlessTree::detach_core`. +//! Creating such a link works, because a fresh link has `source == 0`. +//! +//! Usage: `cargo run --release -- `, exit code 101 is the panic. +use doublets::{Doublets, mem::Global, split, unit}; + +type Step = (&'static str, u32, u32, u32); + +fn run>(mut store: D, steps: &[Step]) -> u32 { + for &(operation, id, from, to) in steps { + match operation { + "create" => assert_eq!(store.create_link(from, to).unwrap(), id), + "update" => drop(store.update(id, from, to).unwrap()), + _ => drop(store.delete(id).unwrap()), + } + } + store.count() +} + +fn main() { + let scenarios: [(&str, Vec); 6] = [ + ("point: update to itself, delete", vec![("create", 1, 1, 1), ("update", 1, 1, 1), ("delete", 1, 0, 0)]), + ("point: delete", vec![("create", 1, 1, 1), ("delete", 1, 0, 0)]), + ( + "swap a link that references itself, delete", + vec![("create", 1, 1, 1), ("create", 2, 1, 2), ("update", 2, 2, 1), ("delete", 2, 0, 0), ("delete", 1, 0, 0)], + ), + ( + "swap a link that references itself, no delete", + vec![("create", 1, 1, 1), ("create", 2, 1, 2), ("update", 2, 2, 1)], + ), + ( + "swap links without self references, delete", + vec![ + ("create", 1, 1, 1), + ("create", 2, 2, 2), + ("create", 3, 1, 2), + ("create", 4, 3, 1), + ("update", 3, 2, 1), + ("update", 4, 1, 3), + ("delete", 4, 0, 0), + ("delete", 3, 0, 0), + ("delete", 1, 0, 0), + ], + ), + ( + "create a link that references itself, delete", + vec![("create", 1, 1, 1), ("create", 2, 1, 2), ("create", 3, 3, 1), ("delete", 2, 0, 0), ("delete", 3, 0, 0), ("delete", 1, 0, 0)], + ), + ]; + let kind = std::env::args().nth(1).unwrap_or_default(); + let scenario: usize = std::env::args().nth(2).unwrap().parse().unwrap(); + let (name, steps) = &scenarios[scenario]; + print!("{kind:5} {name:40} "); + let count = match kind.as_str() { + "unit" => run(unit::Store::::new(Global::new()).unwrap(), steps), + "dump" => { + let mut store = split::Store::::new(Global::new(), Global::new()).unwrap(); + for &(operation, id, from, to) in steps { + match operation { + "create" => drop(store.create_link(from, to).unwrap()), + "update" => drop(store.update(id, from, to).unwrap()), + _ => drop(store.delete(id).unwrap()), + } + eprintln!("after {operation} {id} {from} {to}:"); + for link in 1..=3 { + eprintln!(" {link}: {:?} {:?} unused={}", store.get_data_part(link), store.get_index_part(link), store.is_unused(link)); + } + } + store.count() + } + _ => run(split::Store::::new(Global::new(), Global::new()).unwrap(), steps), + }; + println!("ok, count = {count}"); +} diff --git a/experiments/sqlite_returning/Cargo.lock b/experiments/sqlite_returning/Cargo.lock new file mode 100644 index 0000000..47452bc --- /dev/null +++ b/experiments/sqlite_returning/Cargo.lock @@ -0,0 +1,283 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "cc" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f74872d07caf508b30a21f6836e7d7016a2eaf7d9ff4f48deaa58cd8a0407630" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + +[[package]] +name = "find-msvc-tools" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashlink" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a596f1b20ed2cc5ecac41a164aaebc7258057060f06c0cf7a2ba3991ee7990fb" +dependencies = [ + "hashbrown 0.17.1", +] + +[[package]] +name = "js-sys" +version = "0.3.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1d20bef17f513b9b3004532233187769cd072d790971f4e4da0e346eb6401e8" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rsqlite-vfs" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c51c9ae4df8a7fba42103df5c621fa3c37eccf3a3c650879e90fc48b11cc192c" +dependencies = [ + "hashbrown 0.16.1", + "thiserror", +] + +[[package]] +name = "rusqlite" +version = "0.40.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23f2a97da3e3873c73cb2a2e71b35c40ff95e0b1eefa8d72d8499a6928c3b5b3" +dependencies = [ + "bitflags", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", + "sqlite-wasm-rs", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "smallvec" +version = "1.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9395f0f0eee849a9b707b2f06bb92a6a422090e2123bb2ef8e87a0e61892a8e" + +[[package]] +name = "sqlite-returning" +version = "0.1.0" +dependencies = [ + "rusqlite", +] + +[[package]] +name = "sqlite-wasm-rs" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc3efc0da82635d7e1ced0053bbbfa8c7ab9645d0bf36ceb4f7127bb85315d75" +dependencies = [ + "cc", + "js-sys", + "rsqlite-vfs", + "wasm-bindgen", +] + +[[package]] +name = "syn" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "wasm-bindgen" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6" +dependencies = [ + "unicode-ident", +] diff --git a/experiments/sqlite_returning/Cargo.toml b/experiments/sqlite_returning/Cargo.toml new file mode 100644 index 0000000..33437c8 --- /dev/null +++ b/experiments/sqlite_returning/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "sqlite-returning" +version = "0.1.0" +edition = "2024" + +[dependencies] +rusqlite = { version = "0.40.2", features = ["bundled"] } + +[workspace] diff --git a/experiments/sqlite_returning/src/main.rs b/experiments/sqlite_returning/src/main.rs new file mode 100644 index 0000000..6e87fd8 --- /dev/null +++ b/experiments/sqlite_returning/src/main.rs @@ -0,0 +1,60 @@ +//! Times 100,000 inserts of a blog post in one transaction with each way of getting the new id. +//! Results with rusqlite 0.40.2 (SQLite 3.53.2): `RETURNING id` makes inserts about 3x slower, so both +//! languages read `sqlite3_last_insert_rowid` instead (C# through SQLitePCLRaw). +//! execute + last_insert_rowid: 1968-2181 ns/insert +//! query_row RETURNING id: 5804-6202 ns/insert +//! query + drain RETURNING id: 5862-6105 ns/insert +use rusqlite::Connection; +use std::time::Instant; + +const N: i64 = 100_000; + +fn main() { + let content = "Lorem ipsum ".repeat(60); + for (name, sql) in [ + ( + "execute + last_insert_rowid", + "INSERT INTO posts (title, content) VALUES (?1, ?2)", + ), + ( + "query_row RETURNING id", + "INSERT INTO posts (title, content) VALUES (?1, ?2) RETURNING id", + ), + ( + "query + drain RETURNING id", + "INSERT INTO posts (title, content) VALUES (?1, ?2) RETURNING id", + ), + ] { + for _ in 0..3 { + let connection = Connection::open_in_memory().unwrap(); + connection + .execute_batch("CREATE TABLE posts (id INTEGER PRIMARY KEY, title TEXT NOT NULL, content TEXT NOT NULL); BEGIN IMMEDIATE") + .unwrap(); + let mut statement = connection.prepare_cached(sql).unwrap(); + let start = Instant::now(); + for i in 1..=N { + let title = format!("Blog post {i}"); + let id: i64 = match name { + "execute + last_insert_rowid" => { + statement.execute((&title, &content)).unwrap(); + connection.last_insert_rowid() + } + "query_row RETURNING id" => statement + .query_row((&title, &content), |row| row.get(0)) + .unwrap(), + _ => { + let mut rows = statement.query((&title, &content)).unwrap(); + let id = rows.next().unwrap().unwrap().get(0).unwrap(); + assert!(rows.next().unwrap().is_none()); + id + } + }; + assert_eq!(id, i); + } + println!( + "{name}: {:.0} ns/insert", + start.elapsed().as_nanos() as f64 / N as f64 + ); + } + } +} diff --git a/experiments/unit_store_growth/Cargo.lock b/experiments/unit_store_growth/Cargo.lock new file mode 100644 index 0000000..43e987e --- /dev/null +++ b/experiments/unit_store_growth/Cargo.lock @@ -0,0 +1,544 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "allocator-api2" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c880a97d28a3681c0267bd29cff89621202715b065127cd445fa0f0fe0aa2880" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "beef" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a8241f3ebb85c056b509d4327ad0358fbbba6ffb340bf388f26350aeda225b1" + +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "cc" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f74872d07caf508b30a21f6836e7d7016a2eaf7d9ff4f48deaa58cd8a0407630" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "doublets" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677be8ee593349204e8e7cbc2398d4b98fb95fd1386f3a7eaf286860a5788c8f" +dependencies = [ + "cfg-if", + "leak_slice", + "platform-data", + "platform-mem", + "platform-num", + "platform-trees", + "tap", + "thiserror", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "find-msvc-tools" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashlink" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a596f1b20ed2cc5ecac41a164aaebc7258057060f06c0cf7a2ba3991ee7990fb" +dependencies = [ + "hashbrown 0.17.1", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + +[[package]] +name = "leak_slice" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecf3387da9fb41906394e1306ddd3cd26dd9b7177af11c19b45b364b743aed26" + +[[package]] +name = "libc" +version = "0.2.190" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78" + +[[package]] +name = "libsqlite3-sys" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1d20bef17f513b9b3004532233187769cd072d790971f4e4da0e346eb6401e8" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "memmap2" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1219ed1b7f229ee7104d281dd01d6802fe28bb6e95d292942c4daacdeb798c0" +dependencies = [ + "libc", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + +[[package]] +name = "platform-data" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6782bc71345465116de96d250a36dcf49336010a2320d958d12a5d4390186c90" +dependencies = [ + "beef", + "platform-num", + "thiserror", +] + +[[package]] +name = "platform-mem" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f27cff7c92440ac926c8c91ea3151db6e52a262f602d0c157f254e422fc15b12" +dependencies = [ + "allocator-api2", + "memmap2", + "tempfile", + "thiserror", +] + +[[package]] +name = "platform-num" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c4ca8e18138b1c90ad802aff931f946a0e6bd760c35af30f1ff2489489ab54a" +dependencies = [ + "num-traits", +] + +[[package]] +name = "platform-trees" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40e25a531617fa762c8505826c930f6c1cfcc226f63dea09882b56ae0b8ed078" +dependencies = [ + "platform-num", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rsqlite-vfs" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c51c9ae4df8a7fba42103df5c621fa3c37eccf3a3c650879e90fc48b11cc192c" +dependencies = [ + "hashbrown 0.16.1", + "thiserror", +] + +[[package]] +name = "rusqlite" +version = "0.40.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23f2a97da3e3873c73cb2a2e71b35c40ff95e0b1eefa8d72d8499a6928c3b5b3" +dependencies = [ + "bitflags", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", + "sqlite-wasm-rs", +] + +[[package]] +name = "rustix" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "smallvec" +version = "1.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9395f0f0eee849a9b707b2f06bb92a6a422090e2123bb2ef8e87a0e61892a8e" + +[[package]] +name = "sqlite-vs-doublets" +version = "0.2.0" +dependencies = [ + "doublets", + "rusqlite", + "serde_json", +] + +[[package]] +name = "sqlite-wasm-rs" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc3efc0da82635d7e1ced0053bbbfa8c7ab9645d0bf36ceb4f7127bb85315d75" +dependencies = [ + "cc", + "js-sys", + "rsqlite-vfs", + "wasm-bindgen", +] + +[[package]] +name = "syn" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom", + "once_cell", + "rustix", + "windows-sys", +] + +[[package]] +name = "thiserror" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" + +[[package]] +name = "unit_store_growth" +version = "0.1.0" +dependencies = [ + "doublets", + "sqlite-vs-doublets", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "wasm-bindgen" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/experiments/unit_store_growth/Cargo.toml b/experiments/unit_store_growth/Cargo.toml new file mode 100644 index 0000000..c7754d1 --- /dev/null +++ b/experiments/unit_store_growth/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "unit_store_growth" +version = "0.1.0" +edition = "2024" +publish = false + +[dependencies] +doublets = "0.5.0" +sqlite-vs-doublets = { path = "../../rust" } + +[workspace] diff --git a/experiments/unit_store_growth/src/main.rs b/experiments/unit_store_growth/src/main.rs new file mode 100644 index 0000000..1c8d450 --- /dev/null +++ b/experiments/unit_store_growth/src/main.rs @@ -0,0 +1,35 @@ +//! Minimal reproduction of a `doublets` 0.5.0 and `platform-mem` 0.3.0 mismatch. +//! +//! `doublets::mem::resize_mem` returns what `RawMem::grow_filled` returns, and the stores use it as +//! their whole memory. `platform-mem` returns only the grown part. `Store::init` grows the memory +//! from 8,192 to 2^20 links, so a fresh store sees 1,040,384 links, while its header says that +//! 2^20 − 1 are reserved: creating link 1,040,384 panics with "Data part should be in data memory" +//! (or "index out of bounds" in a tree). A later growth would leave the store with an empty memory. +//! +//! Usage: `cargo run --release -- `: `bare` panics, `whole` uses +//! `sqlite_vs_doublets::memory::Whole` and creates 2^21 + 1 links, growing the memory twice. +use doublets::{Doublets, Error, mem::Global, unit}; +use sqlite_vs_doublets::memory::volatile; +use std::{env, panic}; + +const LINKS: u64 = (1 << 21) + 1; + +fn create(mut store: impl Doublets) -> Result> { + for i in 1..=LINKS { + if i % 100_000 == 0 || (1_040_380..=1_040_390).contains(&i) { + eprintln!("creating link {i}"); + } + store.create_link(1, i.min(2))?; + } + assert_eq!(store.get_link(1).unwrap().target, 1); + Ok(store.count()) +} + +fn main() { + let result = panic::catch_unwind(|| match env::args().nth(1).as_deref() { + Some("bare") => create(unit::Store::::new(Global::new()).unwrap()), + Some("whole") => create(unit::Store::::new(volatile()).unwrap()), + _ => panic!("usage: unit_store_growth "), + }); + println!("{result:?}"); +} diff --git a/rust/Cargo.lock b/rust/Cargo.lock index cdc7556..1e263b6 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -1,43 +1,18 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] -name = "ahash" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" -dependencies = [ - "getrandom", - "once_cell", - "version_check", -] - -[[package]] -name = "aho-corasick" -version = "0.7.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" -dependencies = [ - "memchr", -] - -[[package]] -name = "atty" -version = "0.2.14" +name = "allocator-api2" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" -dependencies = [ - "hermit-abi 0.1.19", - "libc", - "winapi", -] +checksum = "c880a97d28a3681c0267bd29cff89621202715b065127cd445fa0f0fe0aa2880" [[package]] name = "autocfg" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "beef" @@ -47,27 +22,21 @@ checksum = "3a8241f3ebb85c056b509d4327ad0358fbbba6ffb340bf388f26350aeda225b1" [[package]] name = "bitflags" -version = "1.3.2" +version = "2.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" [[package]] name = "bumpalo" -version = "3.11.1" +version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba" - -[[package]] -name = "cast" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] name = "cc" -version = "1.2.52" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd4932aefd12402b36c60956a4fe0035421f544799057659ff86f923657aada3" +checksum = "f74872d07caf508b30a21f6836e7d7016a2eaf7d9ff4f48deaa58cd8a0407630" dependencies = [ "find-msvc-tools", "shlex", @@ -75,149 +44,41 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" - -[[package]] -name = "clap" -version = "2.34.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" -dependencies = [ - "bitflags", - "textwrap", - "unicode-width", -] - -[[package]] -name = "criterion" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b01d6de93b2b6c65e17c634a26653a29d107b3c98c607c765bf38d041531cd8f" -dependencies = [ - "atty", - "cast", - "clap", - "criterion-plot", - "csv", - "itertools", - "lazy_static", - "num-traits", - "oorandom", - "plotters", - "rayon", - "regex", - "serde", - "serde_cbor", - "serde_derive", - "serde_json", - "tinytemplate", - "walkdir", -] - -[[package]] -name = "criterion-plot" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2673cc8207403546f45f5fd319a974b1e6983ad1a3ee7e6041650013be041876" -dependencies = [ - "cast", - "itertools", -] - -[[package]] -name = "crossbeam-channel" -version = "0.5.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-deque" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" -dependencies = [ - "crossbeam-epoch", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-epoch" -version = "0.9.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" - -[[package]] -name = "csv" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acdc4883a9c96732e4733212c01447ebd805833b7275a73ca3ee080fd77afdaf" -dependencies = [ - "csv-core", - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "csv-core" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "704a3c26996a80471189265814dbc2c257598b96b8a7feae2d31ace646bb9782" -dependencies = [ - "memchr", -] - -[[package]] -name = "delegate" -version = "0.7.0" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d70a2d4995466955a415223acf3c9c934b9ff2339631cdf4ffc893da4bacd717" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" [[package]] name = "doublets" -version = "0.1.0-pre+beta.15" -source = "git+https://github.com/linksplatform/doublets-rs.git?rev=5522d91c536654934b7181829f6efb570fb8bb44#5522d91c536654934b7181829f6efb570fb8bb44" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677be8ee593349204e8e7cbc2398d4b98fb95fd1386f3a7eaf286860a5788c8f" dependencies = [ - "bumpalo", "cfg-if", "leak_slice", "platform-data", "platform-mem", + "platform-num", "platform-trees", "tap", "thiserror", ] [[package]] -name = "either" -version = "1.15.0" +name = "errno" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] [[package]] name = "fallible-iterator" -version = "0.2.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" [[package]] name = "fallible-streaming-iterator" @@ -227,114 +88,76 @@ checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" [[package]] name = "fastrand" -version = "1.8.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499" -dependencies = [ - "instant", -] +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" [[package]] name = "find-msvc-tools" -version = "0.1.7" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f449e6c6c08c865631d4890cfacf252b3d396c9bcc83adb6623cdb02a8336c41" +checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484" [[package]] -name = "funty" -version = "2.0.0" +name = "foldhash" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" [[package]] name = "getrandom" -version = "0.2.8" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ "cfg-if", "libc", - "wasi", + "r-efi", ] -[[package]] -name = "half" -version = "1.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b43ede17f21864e81be2fa654110bf1e793774238d86ef8555c37e6519c0403" - [[package]] name = "hashbrown" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" -dependencies = [ - "ahash", -] - -[[package]] -name = "hashlink" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7249a3129cbc1ffccd74857f81464a323a152173cdb134e0fd81bc803b29facf" -dependencies = [ - "hashbrown", -] - -[[package]] -name = "hermit-abi" -version = "0.1.19" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ - "libc", + "foldhash", ] [[package]] -name = "hermit-abi" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" - -[[package]] -name = "instant" -version = "0.1.13" +name = "hashbrown" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" dependencies = [ - "cfg-if", + "foldhash", ] [[package]] -name = "itertools" -version = "0.10.5" +name = "hashlink" +version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +checksum = "a596f1b20ed2cc5ecac41a164aaebc7258057060f06c0cf7a2ba3991ee7990fb" dependencies = [ - "either", + "hashbrown 0.17.1", ] [[package]] name = "itoa" -version = "1.0.5" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "js-sys" -version = "0.3.61" +version = "0.3.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "445dde2150c55e483f3d8416706b97ec8e8237c307e5b7b4b8dd15e6af2a0730" +checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5" dependencies = [ + "cfg-if", "wasm-bindgen", ] -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - [[package]] name = "leak_slice" version = "0.2.0" @@ -343,15 +166,15 @@ checksum = "ecf3387da9fb41906394e1306ddd3cd26dd9b7177af11c19b45b364b743aed26" [[package]] name = "libc" -version = "0.2.180" +version = "0.2.190" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" +checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78" [[package]] name = "libsqlite3-sys" -version = "0.24.2" +version = "0.38.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "898745e570c7d0453cc1fbc4a701eb6c662ed54e8fec8b7d14be137ebeeb9d14" +checksum = "f1d20bef17f513b9b3004532233187769cd072d790971f4e4da0e346eb6401e8" dependencies = [ "cc", "pkg-config", @@ -359,22 +182,22 @@ dependencies = [ ] [[package]] -name = "log" -version = "0.4.29" +name = "linux-raw-sys" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "memchr" -version = "2.7.6" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "memmap2" -version = "0.5.10" +version = "0.9.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83faa42c0a078c393f6b29d5db232d8be22776a891f8f56e5284faee4a20b327" +checksum = "d1219ed1b7f229ee7104d281dd01d6802fe28bb6e95d292942c4daacdeb798c0" dependencies = [ "libc", ] @@ -388,228 +211,150 @@ dependencies = [ "autocfg", ] -[[package]] -name = "num_cpus" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" -dependencies = [ - "hermit-abi 0.5.2", - "libc", -] - [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" - -[[package]] -name = "oorandom" -version = "11.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "pkg-config" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" [[package]] name = "platform-data" -version = "0.1.0-beta.3" -source = "git+https://github.com/linksplatform/doublets-rs.git?rev=5522d91c536654934b7181829f6efb570fb8bb44#5522d91c536654934b7181829f6efb570fb8bb44" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6782bc71345465116de96d250a36dcf49336010a2320d958d12a5d4390186c90" dependencies = [ "beef", - "funty", + "platform-num", "thiserror", ] [[package]] name = "platform-mem" -version = "0.1.0-pre+beta.2" -source = "git+https://github.com/linksplatform/doublets-rs.git?rev=5522d91c536654934b7181829f6efb570fb8bb44#5522d91c536654934b7181829f6efb570fb8bb44" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f27cff7c92440ac926c8c91ea3151db6e52a262f602d0c157f254e422fc15b12" dependencies = [ - "delegate", + "allocator-api2", "memmap2", - "tap", "tempfile", "thiserror", ] [[package]] -name = "platform-trees" -version = "0.1.0-beta.1" -source = "git+https://github.com/linksplatform/doublets-rs.git?rev=5522d91c536654934b7181829f6efb570fb8bb44#5522d91c536654934b7181829f6efb570fb8bb44" -dependencies = [ - "funty", - "platform-data", -] - -[[package]] -name = "plotters" -version = "0.3.6" +name = "platform-num" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a15b6eccb8484002195a3e44fe65a4ce8e93a625797a063735536fd59cb01cf3" +checksum = "2c4ca8e18138b1c90ad802aff931f946a0e6bd760c35af30f1ff2489489ab54a" dependencies = [ "num-traits", - "plotters-backend", - "plotters-svg", - "wasm-bindgen", - "web-sys", ] [[package]] -name = "plotters-backend" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" - -[[package]] -name = "plotters-svg" -version = "0.3.7" +name = "platform-trees" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" +checksum = "40e25a531617fa762c8505826c930f6c1cfcc226f63dea09882b56ae0b8ed078" dependencies = [ - "plotters-backend", + "platform-num", ] [[package]] name = "proc-macro2" -version = "1.0.36" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" dependencies = [ - "unicode-xid", + "unicode-ident", ] [[package]] name = "quote" -version = "1.0.15" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "864d3e96a899863136fc6e99f3d7cae289dafe43bf2c5ac19b70df7210c0a145" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" dependencies = [ "proc-macro2", ] [[package]] -name = "rayon" -version = "1.6.1" +name = "r-efi" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db3a213adf02b3bcfd2d3846bb41cb22857d131789e01df434fb7e7bc0759b7" -dependencies = [ - "either", - "rayon-core", -] +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] -name = "rayon-core" -version = "1.10.2" +name = "rsqlite-vfs" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "356a0625f1954f730c0201cdab48611198dc6ce21f4acff55089b5a78e6e835b" +checksum = "c51c9ae4df8a7fba42103df5c621fa3c37eccf3a3c650879e90fc48b11cc192c" dependencies = [ - "crossbeam-channel", - "crossbeam-deque", - "crossbeam-utils", - "num_cpus", -] - -[[package]] -name = "redox_syscall" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" -dependencies = [ - "bitflags", -] - -[[package]] -name = "regex" -version = "1.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48aaa5748ba571fb95cd2c85c09f629215d3a6ece942baa100950af03a34f733" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" -dependencies = [ - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.6.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" - -[[package]] -name = "remove_dir_all" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" -dependencies = [ - "winapi", + "hashbrown 0.16.1", + "thiserror", ] [[package]] name = "rusqlite" -version = "0.27.0" +version = "0.40.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85127183a999f7db96d1a976a309eebbfb6ea3b0b400ddd8340190129de6eb7a" +checksum = "23f2a97da3e3873c73cb2a2e71b35c40ff95e0b1eefa8d72d8499a6928c3b5b3" dependencies = [ "bitflags", "fallible-iterator", "fallible-streaming-iterator", "hashlink", "libsqlite3-sys", - "memchr", "smallvec", + "sqlite-wasm-rs", ] [[package]] -name = "ryu" -version = "1.0.12" +name = "rustix" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b4b9743ed687d4b4bcedf9ff5eaa7398495ae14e61cba0a295704edbc7decde" +checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] [[package]] -name = "same-file" -version = "1.0.6" +name = "rustversion" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" [[package]] name = "serde" -version = "1.0.185" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be9b6f69f1dfd54c3b568ffa45c310d6973a5e5148fd40cf515acaf38cf5bc31" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", +] [[package]] -name = "serde_cbor" -version = "0.11.2" +name = "serde_core" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bef2ebfde456fb76bbcf9f59315333decc4fda0b2b44b420243c11e0f5ec1f5" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" dependencies = [ - "half", - "serde", + "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.136" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", @@ -618,65 +363,59 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.109" +version = "1.0.151" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb0652c533506ad7a2e353cce269330d6afd8bdfb6d75e0ace5b35aacbd7b9e9" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" dependencies = [ "itoa", - "ryu", + "memchr", "serde", + "serde_core", + "zmij", ] [[package]] name = "shlex" -version = "1.3.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" [[package]] name = "smallvec" -version = "1.15.1" +version = "1.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +checksum = "f9395f0f0eee849a9b707b2f06bb92a6a422090e2123bb2ef8e87a0e61892a8e" [[package]] name = "sqlite-vs-doublets" -version = "0.1.0" +version = "0.2.0" dependencies = [ - "bumpalo", - "criterion", "doublets", - "fastrand", - "getrandom", - "itoa", - "once_cell", - "proc-macro2", - "quote", - "rayon", - "rayon-core", - "regex", - "regex-automata", - "regex-syntax", "rusqlite", - "ryu", - "same-file", - "serde_derive", - "syn", - "tempfile", - "unicode-width", - "walkdir", - "winapi-util", + "serde_json", +] + +[[package]] +name = "sqlite-wasm-rs" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc3efc0da82635d7e1ced0053bbbfa8c7ab9645d0bf36ceb4f7127bb85315d75" +dependencies = [ + "cc", + "js-sys", + "rsqlite-vfs", + "wasm-bindgen", ] [[package]] name = "syn" -version = "1.0.86" +version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a65b3f4ffa0092e9887669db0eae07941f023991ab58ea44da8fe8e2d511c6b" +checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee" dependencies = [ "proc-macro2", "quote", - "unicode-xid", + "unicode-ident", ] [[package]] @@ -687,41 +426,31 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "tempfile" -version = "3.3.0" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ - "cfg-if", "fastrand", - "libc", - "redox_syscall", - "remove_dir_all", - "winapi", -] - -[[package]] -name = "textwrap" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" -dependencies = [ - "unicode-width", + "getrandom", + "once_cell", + "rustix", + "windows-sys", ] [[package]] name = "thiserror" -version = "1.0.39" +version = "2.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5ab016db510546d856297882807df8da66a16fb8c4101cb8b30054b0d5b2d9c" +checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.39" +version = "2.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5420d42e90af0c38c3290abcca25b9b3bdf379fc9f55c528f53a269d9c9a267e" +checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" dependencies = [ "proc-macro2", "quote", @@ -729,26 +458,10 @@ dependencies = [ ] [[package]] -name = "tinytemplate" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" -dependencies = [ - "serde", - "serde_json", -] - -[[package]] -name = "unicode-width" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" - -[[package]] -name = "unicode-xid" -version = "0.2.6" +name = "unicode-ident" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" [[package]] name = "vcpkg" @@ -756,59 +469,24 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "walkdir" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" -dependencies = [ - "same-file", - "winapi", - "winapi-util", -] - -[[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" - [[package]] name = "wasm-bindgen" -version = "0.2.84" +version = "0.2.129" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b" +checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409" dependencies = [ "cfg-if", - "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.84" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9" -dependencies = [ - "bumpalo", - "log", "once_cell", - "proc-macro2", - "quote", - "syn", + "rustversion", + "wasm-bindgen-macro", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.84" +version = "0.2.129" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5" +checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -816,60 +494,43 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.84" +version = "0.2.129" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6" +checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7" dependencies = [ + "bumpalo", "proc-macro2", "quote", "syn", - "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.84" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d" - -[[package]] -name = "web-sys" -version = "0.3.61" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e33b99f4b23ba3eec1a53ac264e35a755f00e966e0065077d6027c0f575b0b97" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "winapi" -version = "0.3.9" +version = "0.2.129" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6" dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", + "unicode-ident", ] [[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" +name = "windows-link" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] -name = "winapi-util" -version = "0.1.5" +name = "windows-sys" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ - "winapi", + "windows-link", ] [[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" +name = "zmij" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 463bfc8..f117b96 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -1,42 +1,14 @@ [package] name = "sqlite-vs-doublets" -version = "0.1.0" -edition = "2021" +version = "0.2.0" +edition = "2024" license = "Unlicense" -description = "Benchmark comparing SQLite vs Doublets performance for basic CRUD operations" - -[[bench]] -name = "bench" -harness = false +description = "Compares SQLite and Doublets as storage for links and for objects" [dependencies] -rusqlite = { version = "=0.27.0", features = ["bundled"] } -doublets = { git = "https://github.com/linksplatform/doublets-rs.git", rev = "5522d91c536654934b7181829f6efb570fb8bb44" } -once_cell = "1.14" - -[dev-dependencies] -criterion = { version = "=0.3.6", default-features = false } - -# Pin dependencies for compatibility with nightly-2022-08-22 (rustc 1.65) -serde_derive = "=1.0.136" -proc-macro2 = "=1.0.36" -quote = "=1.0.15" -syn = "=1.0.86" -bumpalo = "=3.11.1" -rayon = "=1.6.1" -rayon-core = "=1.10.2" -itoa = "=1.0.5" -ryu = "=1.0.12" -unicode-width = "=0.1.10" -tempfile = "=3.3.0" -fastrand = "=1.8.0" -getrandom = "=0.2.8" -winapi-util = "=0.1.5" -walkdir = "=2.3.2" -same-file = "=1.0.6" -regex = "=1.7.1" -regex-automata = "=0.1.10" -regex-syntax = "=0.6.28" +doublets = "0.5.0" +rusqlite = { version = "0.40.2", features = ["bundled", "fallible_uint"] } +serde_json = "1.0" [profile.release] lto = true diff --git a/rust/benches/bench.rs b/rust/benches/bench.rs deleted file mode 100644 index b5e0f47..0000000 --- a/rust/benches/bench.rs +++ /dev/null @@ -1,29 +0,0 @@ -//! SQLite versus Doublets benchmark suite. -//! -//! Preparation is performed by `benchmarks::measure` outside the measured -//! region. Every operation is run against all six storage variants. - -#![feature(allocator_api)] - -mod benchmarks; - -use criterion::{criterion_group, criterion_main}; - -criterion_group! { - name = benches; - config = benchmarks::configure_criterion(); - targets = - benchmarks::links::create, - benchmarks::links::update, - benchmarks::links::delete, - benchmarks::each::all, - benchmarks::each::identity, - benchmarks::each::concrete, - benchmarks::each::outgoing, - benchmarks::each::incoming, - benchmarks::objects::create, - benchmarks::objects::read, - benchmarks::objects::delete, -} - -criterion_main!(benches); diff --git a/rust/benches/benchmarks/each.rs b/rust/benches/benchmarks/each.rs deleted file mode 100644 index 626ac20..0000000 --- a/rust/benches/benchmarks/each.rs +++ /dev/null @@ -1,51 +0,0 @@ -//! Link enumeration and restriction benchmarks. - -use criterion::Criterion; -use sqlite_vs_doublets::{ - fill_background, fill_benchmarked, Links, BACKGROUND_LINK_COUNT, BENCHMARK_LINK_COUNT, -}; - -macro_rules! query_benchmark { - ($function:ident, $group:literal, $query:expr) => { - pub fn $function(criterion: &mut Criterion) { - benchmark_operation!( - criterion, - $group, - *BENCHMARK_LINK_COUNT, - |storage| { - let background = fill_background(storage, *BACKGROUND_LINK_COUNT); - fill_benchmarked(storage, &background, *BENCHMARK_LINK_COUNT) - }, - $query, - ); - } - }; -} - -query_benchmark!(all, "query_all", |storage, _created| { - criterion::black_box(storage.query_all()); -}); - -query_benchmark!(identity, "query_by_id", |storage, created| { - for link in created { - criterion::black_box(storage.query_by_id(link.id)); - } -}); - -query_benchmark!(concrete, "query_by_source_target", |storage, created| { - for link in created { - criterion::black_box(storage.query_by_source_target(link.source, link.target)); - } -}); - -query_benchmark!(outgoing, "query_by_source", |storage, created| { - for link in created { - criterion::black_box(storage.query_by_source(link.source)); - } -}); - -query_benchmark!(incoming, "query_by_target", |storage, created| { - for link in created { - criterion::black_box(storage.query_by_target(link.target)); - } -}); diff --git a/rust/benches/benchmarks/links.rs b/rust/benches/benchmarks/links.rs deleted file mode 100644 index 6fe6815..0000000 --- a/rust/benches/benchmarks/links.rs +++ /dev/null @@ -1,57 +0,0 @@ -//! Create, update and delete benchmarks. - -use criterion::Criterion; -use sqlite_vs_doublets::{ - fill_background, fill_benchmarked, Links, BACKGROUND_LINK_COUNT, BENCHMARK_LINK_COUNT, -}; - -pub fn create(criterion: &mut Criterion) { - benchmark_operation!( - criterion, - "create", - *BENCHMARK_LINK_COUNT, - |storage| fill_background(storage, *BACKGROUND_LINK_COUNT), - |storage, background| { - let created = fill_benchmarked(storage, &background, *BENCHMARK_LINK_COUNT); - criterion::black_box(created); - }, - ); -} - -pub fn update(criterion: &mut Criterion) { - benchmark_operation!( - criterion, - "update", - *BENCHMARK_LINK_COUNT, - |storage| { - let background = fill_background(storage, *BACKGROUND_LINK_COUNT); - let created = fill_benchmarked(storage, &background, *BENCHMARK_LINK_COUNT); - let update_sources = fill_background(storage, *BENCHMARK_LINK_COUNT); - (background, created, update_sources) - }, - |storage, (background, created, update_sources)| { - for (index, link) in created.into_iter().enumerate() { - // A dedicated source makes the new doublet unique. Updating to - // an existing pair is not a valid Doublets operation. - storage.update(link.id, update_sources[index], background[0]); - } - }, - ); -} - -pub fn delete(criterion: &mut Criterion) { - benchmark_operation!( - criterion, - "delete", - *BENCHMARK_LINK_COUNT, - |storage| { - let background = fill_background(storage, *BACKGROUND_LINK_COUNT); - fill_benchmarked(storage, &background, *BENCHMARK_LINK_COUNT) - }, - |storage, created| { - for link in created { - storage.delete(link.id); - } - }, - ); -} diff --git a/rust/benches/benchmarks/mod.rs b/rust/benches/benchmarks/mod.rs deleted file mode 100644 index e884829..0000000 --- a/rust/benches/benchmarks/mod.rs +++ /dev/null @@ -1,157 +0,0 @@ -//! Shared infrastructure of the benchmark suite. -//! -//! Every operation is measured on the same six subjects, so the modules below -//! only describe *what* is prepared and *what* is measured, and the -//! [`benchmark_operation`] macro takes care of running it everywhere: -//! -//! | Subject | Database | Memory | -//! |-------------------------------|----------|---------------| -//! | `SQLite_Memory` | SQLite | volatile | -//! | `SQLite_File` | SQLite | non-volatile | -//! | `Doublets_United_Volatile` | Doublets | volatile | -//! | `Doublets_United_NonVolatile` | Doublets | non-volatile | -//! | `Doublets_Split_Volatile` | Doublets | volatile | -//! | `Doublets_Split_NonVolatile` | Doublets | non-volatile | -//! -//! The data of an iteration is prepared *outside* of the measured region with -//! [`Criterion::iter_custom`](criterion::Bencher::iter_custom), so that the -//! reported time is the time of the operation itself and not the time of -//! filling the storage. - -use criterion::{Bencher, Criterion}; -use sqlite_vs_doublets::{Benched, Links, Objects}; -use std::{ - env, - ops::DerefMut, - time::{Duration, Instant}, -}; - -/// Prepares the storage of every iteration and measures `operation` on it. -/// -/// `prepare` runs on an empty (forked) storage and its result is handed to -/// `operation`, which is the only part of the iteration that is timed. Dropping -/// the fork empties the storage again for the next iteration. -pub fn measure(bencher: &mut Bencher<'_>, mut prepare: P, mut operation: O) -where - B: Benched + DerefMut, - B::Target: Links + Objects + Sized, - P: FnMut(&mut B::Target) -> S, - O: FnMut(&mut B::Target, S), -{ - let mut benched = B::setup(()); - bencher.iter_custom(|iterations| { - let mut elapsed = Duration::ZERO; - for _ in 0..iterations { - let mut fork = benched.fork(); - let storage = &mut **fork; - let prepared = prepare(storage); - - let start = Instant::now(); - operation(storage, prepared); - elapsed += start.elapsed(); - } - elapsed - }); -} - -/// Measures one operation on one subject. -macro_rules! benchmark_subject { - ($group:expr, $count:expr, $name:literal, $subject:ty, $prepare:expr, $operation:expr) => { - $group.bench_with_input( - criterion::BenchmarkId::new($name, $count), - &$count, - |bencher, _| { - crate::benchmarks::measure::<$subject, _, _, _>(bencher, $prepare, $operation) - }, - ); - }; -} - -/// Measures one operation on every subject of the comparison. -/// -/// `$prepare` and `$operation` are repeated for each subject, so that both -/// closures are inferred against the storage type of that subject. -macro_rules! benchmark_operation { - ($criterion:expr, $name:literal, $count:expr, $prepare:expr, $operation:expr $(,)?) => {{ - let count = $count; - let mut group = $criterion.benchmark_group($name); - group.throughput(criterion::Throughput::Elements(count as u64)); - benchmark_subject!( - group, - count, - "SQLite_Memory", - sqlite_vs_doublets::benched::SqliteMemoryBenched, - $prepare, - $operation - ); - benchmark_subject!( - group, - count, - "SQLite_File", - sqlite_vs_doublets::benched::SqliteFileBenched, - $prepare, - $operation - ); - benchmark_subject!( - group, - count, - "Doublets_United_Volatile", - sqlite_vs_doublets::benched::DoubletsUnitedVolatileBenched, - $prepare, - $operation - ); - benchmark_subject!( - group, - count, - "Doublets_United_NonVolatile", - sqlite_vs_doublets::benched::DoubletsUnitedNonVolatileBenched, - $prepare, - $operation - ); - benchmark_subject!( - group, - count, - "Doublets_Split_Volatile", - sqlite_vs_doublets::benched::DoubletsSplitVolatileBenched, - $prepare, - $operation - ); - benchmark_subject!( - group, - count, - "Doublets_Split_NonVolatile", - sqlite_vs_doublets::benched::DoubletsSplitNonVolatileBenched, - $prepare, - $operation - ); - group.finish(); - }}; -} - -pub mod each; -pub mod links; -pub mod objects; - -fn env_value(name: &str, default: T) -> T { - env::var(name) - .ok() - .and_then(|value| value.parse().ok()) - .unwrap_or(default) -} - -/// Criterion configuration of the suite. -/// -/// The defaults keep a pull request run short; the full run of the `main` -/// branch raises them through the environment. -pub fn configure_criterion() -> Criterion { - Criterion::default() - .sample_size(env_value("BENCHMARK_SAMPLE_SIZE", 10usize)) - .measurement_time(Duration::from_secs(env_value( - "BENCHMARK_MEASUREMENT_SECONDS", - 1u64, - ))) - .warm_up_time(Duration::from_millis(env_value( - "BENCHMARK_WARM_UP_MILLISECONDS", - 500u64, - ))) -} diff --git a/rust/benches/benchmarks/objects.rs b/rust/benches/benchmarks/objects.rs deleted file mode 100644 index c3b29a8..0000000 --- a/rust/benches/benchmarks/objects.rs +++ /dev/null @@ -1,44 +0,0 @@ -//! Object-like (blog post) benchmarks. - -use criterion::Criterion; -use sqlite_vs_doublets::{generate_posts, Objects, BENCHMARK_OBJECT_COUNT}; - -pub fn create(criterion: &mut Criterion) { - benchmark_operation!( - criterion, - "objects_create", - *BENCHMARK_OBJECT_COUNT, - |_storage| generate_posts(*BENCHMARK_OBJECT_COUNT), - |storage, posts| { - criterion::black_box(storage.create_posts(&posts)); - }, - ); -} - -pub fn read(criterion: &mut Criterion) { - benchmark_operation!( - criterion, - "objects_read", - *BENCHMARK_OBJECT_COUNT, - |storage| { - let posts = generate_posts(*BENCHMARK_OBJECT_COUNT); - storage.create_posts(&posts); - }, - |storage, _prepared| { - criterion::black_box(storage.read_posts()); - }, - ); -} - -pub fn delete(criterion: &mut Criterion) { - benchmark_operation!( - criterion, - "objects_delete", - *BENCHMARK_OBJECT_COUNT, - |storage| { - let posts = generate_posts(*BENCHMARK_OBJECT_COUNT); - storage.create_posts(&posts); - }, - |storage, _prepared| storage.delete_posts(), - ); -} diff --git a/rust/examples/inspect_storage.rs b/rust/examples/inspect_storage.rs deleted file mode 100644 index 0e87403..0000000 --- a/rust/examples/inspect_storage.rs +++ /dev/null @@ -1,76 +0,0 @@ -//! Prints what every benchmarked storage contains after a benchmark iteration. -//! -//! Useful to check by hand that all six subjects of the benchmarks behave the -//! same way — the assumption the whole comparison is based on. -//! -//! ```text -//! cargo run --release --example inspect_storage -//! ``` - -#![feature(allocator_api)] - -use sqlite_vs_doublets::benched::{ - DoubletsSplitNonVolatileBenched, DoubletsSplitVolatileBenched, - DoubletsUnitedNonVolatileBenched, DoubletsUnitedVolatileBenched, SqliteFileBenched, - SqliteMemoryBenched, -}; -use sqlite_vs_doublets::{ - fill_background, fill_benchmarked, generate_posts, Benched, Links, Objects, -}; -use std::ops::DerefMut; - -const BACKGROUND: usize = 100; -const BENCHMARKED: usize = 100; -const POSTS: usize = 10; - -fn inspect(name: &str, subject: &mut B) -where - B: Benched + DerefMut, - B::Target: Links + Objects + Sized, -{ - let mut fork = subject.fork(); - let storage = &mut **fork; - - let background = fill_background(storage, BACKGROUND); - let created = fill_benchmarked(storage, &background, BENCHMARKED); - let sample = created[created.len() / 2]; - let posts = generate_posts(POSTS); - storage.create_posts(&posts); - - println!("{name}:"); - println!(" links {}", storage.count()); - println!( - " by identity {:?}", - storage.query_by_id(sample.id) - ); - let outgoing = storage.query_by_source(sample.source).len(); - println!(" outgoing of the sample {outgoing}"); - let incoming = storage.query_by_target(sample.target).len(); - println!(" incoming of the sample {incoming}"); - println!(" blog posts {}", storage.count_posts()); - println!( - " first blog post {:?}", - storage.read_posts().first() - ); -} - -fn main() { - inspect("SQLite_Memory", &mut SqliteMemoryBenched::setup(())); - inspect("SQLite_File", &mut SqliteFileBenched::setup(())); - inspect( - "Doublets_United_Volatile", - &mut DoubletsUnitedVolatileBenched::setup(()), - ); - inspect( - "Doublets_United_NonVolatile", - &mut DoubletsUnitedNonVolatileBenched::setup(()), - ); - inspect( - "Doublets_Split_Volatile", - &mut DoubletsSplitVolatileBenched::setup(()), - ); - inspect( - "Doublets_Split_NonVolatile", - &mut DoubletsSplitNonVolatileBenched::setup(()), - ); -} diff --git a/rust/out.py b/rust/out.py deleted file mode 100644 index 440e743..0000000 --- a/rust/out.py +++ /dev/null @@ -1,98 +0,0 @@ -#!/usr/bin/env python3 -"""Parse Criterion bencher output and publish the Rust benchmark report.""" - -import argparse -import os -import re -import sys - -REPOSITORY_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -sys.path.insert(0, os.path.join(REPOSITORY_ROOT, "scripts")) - -import benchmark_report as report # noqa: E402 - - -# Criterion writes the beginning and end of a bencher record separately. Error -# text can therefore occur between ``test ...`` and ``bench: ...``. Stop at the -# next test record so an aborted benchmark cannot borrow its successor's value. -BENCHER_PATTERN = re.compile( - r"test\s+(\w+)/(\w+)/(\d+)\s+\.\.\.\s*" - r"(?:(?!\btest\s)[\s\S])*?" - r"bench:\s+([\d,]+)\s+ns/iter" -) - - -def parse_text(content): - """Parse bencher text into ``{operation: {variant: ns_per_iteration}}``.""" - results = report.empty_results() - for match in BENCHER_PATTERN.finditer(content): - operation, variant, _size, nanoseconds = match.groups() - if operation in results: - results[operation][variant] = int(nanoseconds.replace(",", "")) - return results - - -def parse_results(path): - if not os.path.exists(path): - return report.empty_results() - with open(path, "r", encoding="utf-8") as handle: - return parse_text(handle.read()) - - -def parse_args(argv): - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("input", nargs="?", default="out.txt") - parser.add_argument("--results", default="results.md") - parser.add_argument("--readme", action="append", default=[]) - parser.add_argument("--docs-dir") - parser.add_argument("--output-dir", default="") - return parser.parse_args(argv) - - -def main(argv=None): - args = parse_args(argv if argv is not None else sys.argv[1:]) - results = parse_results(args.input) - if not report.has_any_results(results): - print(f"No benchmark data found in {args.input}") - print(report.report_input_excerpt(args.input)) - return 1 - - missing = report.missing_measurements(results) - if missing: - print("Benchmark output is incomplete; missing:") - print("\n".join(missing)) - return 1 - - provenance = report.build_provenance(language="Rust") - section = report.render_results_section(results, provenance) - section += ( - "\n\n![Rust benchmark comparison](docs/benchmarks/bench_rust.png)" - "\n\n![Rust benchmark comparison, logarithmic scale]" - "(docs/benchmarks/bench_rust_log_scale.png)" - ) - with open(args.results, "w", encoding="utf-8") as handle: - handle.write(section + "\n") - print(report.format_results_table(results)) - print(f"Generated {args.results}") - - charts = report.generate_charts( - results, - "bench_rust", - "Benchmark Comparison: SQLite vs Doublets (Rust)", - args.output_dir, - ) - report.copy_charts(charts, args.docs_dir) - - for readme in args.readme: - changed = report.update_markers( - readme, - section, - report.RUST_START_MARKER, - report.RUST_END_MARKER, - ) - print(f"{'Updated' if changed else 'No changes needed in'} {readme}") - return 0 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/rust/results.md b/rust/results.md deleted file mode 100644 index 2e81ed8..0000000 --- a/rust/results.md +++ /dev/null @@ -1,19 +0,0 @@ -_Generated 2026-09-19 18:46 UTC for Rust by [GitHub Actions run 35459522519](https://github.com/linksplatform/Comparisons.SQLiteVSDoublets/actions/runs/35459522519) — 1000 benchmarked links, 3000 background links, 1000 objects._ - -| Operation | Doublets United Volatile | Doublets United NonVolatile | Doublets Split Volatile | Doublets Split NonVolatile | SQLite Memory | SQLite File | -|---------------------|--------------------------|-----------------------------|-------------------------|----------------------------|---------------|-------------| -| Create | 141747 (40.7x faster) | 143659 (40.2x faster) | 69524 (83.0x faster) | 69702 (82.8x faster) | 5772002 | 993908545 | -| Update | 283809 (29.6x faster) | 277912 (30.2x faster) | 63882 (131.3x faster) | 72899 (115.1x faster) | 8390541 | 1067410860 | -| Delete | 168149 (26.8x faster) | 166496 (27.1x faster) | 77711 (58.0x faster) | 78217 (57.6x faster) | 4509130 | 977528491 | -| Each All | 31671 (15.9x faster) | 31638 (15.9x faster) | 34426 (14.6x faster) | 34487 (14.6x faster) | 504031 | 595624 | -| Each Identity | 2132 (1269.3x faster) | 2136 (1266.9x faster) | 2130 (1270.5x faster) | 2127 (1272.3x faster) | 2706180 | 10678850 | -| Each Concrete | 124784 (34.7x faster) | 120544 (36.0x faster) | 45647 (95.0x faster) | 43793 (99.0x faster) | 4335748 | 12668554 | -| Each Outgoing | 210057 (18.1x faster) | 208064 (18.2x faster) | 44112 (86.0x faster) | 45302 (83.7x faster) | 3792959 | 12033519 | -| Each Incoming | 212266 (19.3x faster) | 210393 (19.4x faster) | 50873 (80.4x faster) | 46396 (88.1x faster) | 4089215 | 12477405 | -| Objects Create List | 8759735 (10.5x slower) | 8759735 (10.5x slower) | 5388349 (6.5x slower) | 5436404 (6.5x slower) | 832974 | 2098461 | -| Objects Read List | 5889568 (18.5x slower) | 5585959 (17.6x slower) | 5393697 (17.0x slower) | 4889083 (15.4x slower) | 317868 | 343282 | -| Objects Delete List | 2709133 (75.1x slower) | 2667861 (74.0x slower) | 1083231 (30.0x slower) | 1106546 (30.7x slower) | 36069 | 992614 | - -![Rust benchmark comparison](docs/benchmarks/bench_rust.png) - -![Rust benchmark comparison, logarithmic scale](docs/benchmarks/bench_rust_log_scale.png) diff --git a/rust/rust-toolchain.toml b/rust/rust-toolchain.toml deleted file mode 100644 index 33c06c9..0000000 --- a/rust/rust-toolchain.toml +++ /dev/null @@ -1,3 +0,0 @@ -[toolchain] -channel = "nightly-2022-08-22" -components = ["rustfmt", "clippy"] diff --git a/rust/rustfmt.toml b/rust/rustfmt.toml index 79f8a99..f8d084c 100644 --- a/rust/rustfmt.toml +++ b/rust/rustfmt.toml @@ -1,2 +1,2 @@ -edition = "2021" +edition = "2024" tab_spaces = 4 diff --git a/rust/src/benched/doublets_benched.rs b/rust/src/benched/doublets_benched.rs deleted file mode 100644 index 8777b4e..0000000 --- a/rust/src/benched/doublets_benched.rs +++ /dev/null @@ -1,190 +0,0 @@ -//! Benched implementations for Doublets - -use crate::doublets_impl::{ - create_split_non_volatile, create_split_volatile, create_united_non_volatile, - create_united_volatile, DoubletsLinks, DoubletsSplitNonVolatile, DoubletsSplitVolatile, - DoubletsUnitedNonVolatile, DoubletsUnitedVolatile, -}; -use crate::{temp_path, Benched, Fork, Links}; -use std::path::PathBuf; - -/// Benched implementation for Doublets United (unit) store with volatile storage -pub struct DoubletsUnitedVolatileBenched { - links: DoubletsLinks, -} - -impl Benched for DoubletsUnitedVolatileBenched { - type Builder = (); - - fn setup(_builder: Self::Builder) -> Self { - Self { - links: create_united_volatile(), - } - } - - fn fork(&mut self) -> Fork { - Fork::new(self) - } - - unsafe fn unfork(&mut self) { - self.links.delete_all(); - } -} - -impl std::ops::Deref for DoubletsUnitedVolatileBenched { - type Target = DoubletsLinks; - - fn deref(&self) -> &Self::Target { - &self.links - } -} - -impl std::ops::DerefMut for DoubletsUnitedVolatileBenched { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.links - } -} - -/// Benched implementation for Doublets Split store with volatile storage -pub struct DoubletsSplitVolatileBenched { - links: DoubletsLinks, -} - -impl Benched for DoubletsSplitVolatileBenched { - type Builder = (); - - fn setup(_builder: Self::Builder) -> Self { - Self { - links: create_split_volatile(), - } - } - - fn fork(&mut self) -> Fork { - Fork::new(self) - } - - unsafe fn unfork(&mut self) { - self.links.delete_all(); - } -} - -impl std::ops::Deref for DoubletsSplitVolatileBenched { - type Target = DoubletsLinks; - - fn deref(&self) -> &Self::Target { - &self.links - } -} - -impl std::ops::DerefMut for DoubletsSplitVolatileBenched { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.links - } -} - -/// Benched implementation for Doublets United (unit) store with non-volatile storage -/// -/// The links are stored in a memory-mapped file inside the temporary directory -/// of the machine, which is removed when the benchmark subject is dropped. -pub struct DoubletsUnitedNonVolatileBenched { - links: DoubletsLinks, - path: PathBuf, -} - -impl Benched for DoubletsUnitedNonVolatileBenched { - type Builder = (); - - fn setup(_builder: Self::Builder) -> Self { - let path = temp_path("united.links"); - // Start from an empty store even if a previous run left a file. - let _ = std::fs::remove_file(&path); - Self { - links: create_united_non_volatile(&path), - path, - } - } - - fn fork(&mut self) -> Fork { - Fork::new(self) - } - - unsafe fn unfork(&mut self) { - self.links.delete_all(); - } -} - -impl std::ops::Deref for DoubletsUnitedNonVolatileBenched { - type Target = DoubletsLinks; - - fn deref(&self) -> &Self::Target { - &self.links - } -} - -impl std::ops::DerefMut for DoubletsUnitedNonVolatileBenched { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.links - } -} - -impl Drop for DoubletsUnitedNonVolatileBenched { - fn drop(&mut self) { - let _ = std::fs::remove_file(&self.path); - } -} - -/// Benched implementation for Doublets Split store with non-volatile storage -/// -/// The data and the index are stored in two memory-mapped files inside the -/// temporary directory of the machine, both removed when the benchmark subject -/// is dropped. -pub struct DoubletsSplitNonVolatileBenched { - links: DoubletsLinks, - data_path: PathBuf, - index_path: PathBuf, -} - -impl Benched for DoubletsSplitNonVolatileBenched { - type Builder = (); - - fn setup(_builder: Self::Builder) -> Self { - let data_path = temp_path("split.data.links"); - let index_path = temp_path("split.index.links"); - let _ = std::fs::remove_file(&data_path); - let _ = std::fs::remove_file(&index_path); - Self { - links: create_split_non_volatile(&data_path, &index_path), - data_path, - index_path, - } - } - - fn fork(&mut self) -> Fork { - Fork::new(self) - } - - unsafe fn unfork(&mut self) { - self.links.delete_all(); - } -} - -impl std::ops::Deref for DoubletsSplitNonVolatileBenched { - type Target = DoubletsLinks; - - fn deref(&self) -> &Self::Target { - &self.links - } -} - -impl std::ops::DerefMut for DoubletsSplitNonVolatileBenched { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.links - } -} - -impl Drop for DoubletsSplitNonVolatileBenched { - fn drop(&mut self) { - let _ = std::fs::remove_file(&self.data_path); - let _ = std::fs::remove_file(&self.index_path); - } -} diff --git a/rust/src/benched/mod.rs b/rust/src/benched/mod.rs deleted file mode 100644 index 3e5be02..0000000 --- a/rust/src/benched/mod.rs +++ /dev/null @@ -1,27 +0,0 @@ -//! Benched trait and implementations for SQLite and Doublets - -mod doublets_benched; -mod sqlite_benched; - -pub use doublets_benched::*; -pub use sqlite_benched::*; - -use crate::Fork; - -/// Trait for types that can be benchmarked with setup/teardown lifecycle -pub trait Benched: Sized { - /// The builder type used to construct this benched type - type Builder; - - /// Setup the benchmark environment - fn setup(builder: Self::Builder) -> Self; - - /// Create a fork for an isolated benchmark iteration - fn fork(&mut self) -> Fork; - - /// Clean up after a benchmark iteration - /// - /// # Safety - /// This should only be called by Fork's Drop implementation - unsafe fn unfork(&mut self); -} diff --git a/rust/src/benched/sqlite_benched.rs b/rust/src/benched/sqlite_benched.rs deleted file mode 100644 index 6e1b8c1..0000000 --- a/rust/src/benched/sqlite_benched.rs +++ /dev/null @@ -1,93 +0,0 @@ -//! Benched implementation for SQLite - -use crate::sqlite_impl::SqliteLinks; -use crate::{temp_path, Benched, Fork}; -use std::path::PathBuf; - -/// Benched implementation for SQLite with in-memory database -pub struct SqliteMemoryBenched { - links: SqliteLinks, -} - -impl Benched for SqliteMemoryBenched { - type Builder = (); - - fn setup(_builder: Self::Builder) -> Self { - Self { - links: SqliteLinks::new_memory(), - } - } - - fn fork(&mut self) -> Fork { - Fork::new(self) - } - - unsafe fn unfork(&mut self) { - self.links.reset(); - } -} - -impl std::ops::Deref for SqliteMemoryBenched { - type Target = SqliteLinks; - - fn deref(&self) -> &Self::Target { - &self.links - } -} - -impl std::ops::DerefMut for SqliteMemoryBenched { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.links - } -} - -/// Benched implementation for SQLite with a file based database -/// -/// The file lives in the temporary directory of the machine and is removed when -/// the benchmark subject is dropped. -pub struct SqliteFileBenched { - links: SqliteLinks, - path: PathBuf, -} - -impl Benched for SqliteFileBenched { - type Builder = (); - - fn setup(_builder: Self::Builder) -> Self { - let path = temp_path("sqlite.db"); - // Start from an empty database even if a previous run left a file. - let _ = std::fs::remove_file(&path); - Self { - links: SqliteLinks::new_file(&path), - path, - } - } - - fn fork(&mut self) -> Fork { - Fork::new(self) - } - - unsafe fn unfork(&mut self) { - self.links.reset(); - } -} - -impl std::ops::Deref for SqliteFileBenched { - type Target = SqliteLinks; - - fn deref(&self) -> &Self::Target { - &self.links - } -} - -impl std::ops::DerefMut for SqliteFileBenched { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.links - } -} - -impl Drop for SqliteFileBenched { - fn drop(&mut self) { - let _ = std::fs::remove_file(&self.path); - } -} diff --git a/rust/src/dataset.rs b/rust/src/dataset.rs new file mode 100644 index 0000000..572804a --- /dev/null +++ b/rust/src/dataset.rs @@ -0,0 +1,104 @@ +//! Deterministic data shared by every storage and by the C# implementation. + +use std::sync::{Arc, LazyLock}; + +const GOLDEN_GAMMA: u64 = 0x9E37_79B9_7F4A_7C15; + +pub fn splitmix64(x: u64) -> u64 { + let mut z = x.wrapping_add(GOLDEN_GAMMA); + z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9); + z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB); + z ^ (z >> 31) +} + +/// The `(from, to)` pair of the link with id `i`: both reference already created ids +/// and `from + to == i + 1`, so every pair is unique. +pub fn link(i: u64) -> (u64, u64) { + let from = 1 + splitmix64(2 * i) % i; + (from, i + 1 - from) +} + +/// Order-sensitive mix of two numbers; summing mixes detects records that are lost, +/// duplicated or associated with the wrong id. +pub fn mix(a: u64, b: u64) -> u64 { + (a.wrapping_mul(GOLDEN_GAMMA) ^ b).wrapping_mul(GOLDEN_GAMMA) +} + +pub fn link_checksum(id: u64, from: u64, to: u64) -> u64 { + mix(mix(id, from), to) +} + +pub fn links_checksum(n: u64, swapped: bool) -> u64 { + (1..=n) + .map(|i| { + let (from, to) = link(i); + if swapped { + link_checksum(i, to, from) + } else { + link_checksum(i, from, to) + } + }) + .fold(0, u64::wrapping_add) +} + +/// Visits `1..=n` once each in a scattered order, so point operations do not hit +/// neighbouring records one after another. +pub fn scattered(n: u64) -> impl Iterator { + let mut stride = GOLDEN_GAMMA % n.max(1); + while gcd(stride, n) != 1 { + stride += 1; + } + (0..n).map(move |k| 1 + (k * stride) % n) +} + +fn gcd(a: u64, b: u64) -> u64 { + if b == 0 { a } else { gcd(b, a % b) } +} + +/// Strings are shared like in C#, so neither the dataset nor a sequences cache copies them. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct BlogPost { + pub title: Arc, + pub content: Arc, + /// Seconds since the Unix epoch. + pub publication_date: u64, +} + +impl BlogPost { + pub fn checksum(&self) -> u64 { + let bytes = |text: &str| text.bytes().map(u64::from).sum::(); + bytes(&self.title) + bytes(&self.content) + self.publication_date + } +} + +const SEPTEMBER_2020: u64 = 1_600_000_000; +const THIRTY_DAYS: u64 = 30 * 24 * 60 * 60; + +const PARAGRAPHS: [&str; 5] = [ + "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis malesuada blandit mauris nec bibendum. Phasellus feugiat vehicula mauris et aliquet. Integer et gravida velit, in rutrum leo. Duis pretium, nunc ac posuere porttitor, augue sapien commodo tortor, nec consequat lorem eros ultricies odio. Aliquam varius congue ex nec viverra. Pellentesque eu velit tellus. Donec ac luctus nisi. Curabitur dignissim sodales mauris eu semper. Ut pretium lorem nulla, sit amet auctor arcu placerat vitae. Quisque lacinia dolor et consectetur fermentum. Nam ac orci vitae nulla aliquam tempor ac a nibh. Ut ac tincidunt lacus. Morbi vitae felis lorem.", + "Curabitur tincidunt nibh sit amet finibus dictum. Suspendisse aliquet arcu non rutrum ultrices. Integer ullamcorper mauris sit amet nibh aliquam, et tempor turpis hendrerit. In molestie elit et mauris rutrum, non auctor ligula ultricies. Vestibulum dignissim mauris finibus libero interdum hendrerit. Nunc vitae ipsum porttitor, egestas magna ut, sagittis sem. Donec euismod ac tortor vel porta. Vivamus convallis, ex at vestibulum rutrum, velit purus venenatis metus, sit amet aliquam sapien nibh quis elit. Aenean id neque a orci sodales venenatis. Integer ut orci ligula. Interdum et malesuada fames ac ante ipsum primis in faucibus. Praesent molestie dolor non lobortis ornare. Duis quis nisl sollicitudin, accumsan ante sed, eleifend velit. Maecenas maximus sed ante nec auctor.", + "Donec vitae felis lectus. Aenean velit sapien, porttitor ut feugiat a, consectetur et risus. Proin ac viverra sem. Nullam sagittis ex tortor, eu pellentesque tellus efficitur at. Nunc non egestas leo. Nam sed suscipit neque. Nam sodales vel neque eget eleifend. Vivamus in condimentum elit, consectetur commodo ex. Suspendisse rutrum, sapien efficitur cursus sodales, dolor orci pulvinar mauris, eu fringilla leo ex id leo. Interdum et malesuada fames ac ante ipsum primis in faucibus. Proin rhoncus sapien massa, molestie vestibulum augue hendrerit nec. Aliquam malesuada varius sapien id accumsan. Duis blandit aliquet felis, nec pellentesque lacus tincidunt et. Cras sed ligula vel nisl laoreet sagittis. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Praesent tristique a neque aliquet aliquam.", + "Aliquam sed egestas felis. Maecenas sollicitudin nisl in sapien posuere vulputate. Suspendisse eleifend sem magna, interdum consectetur augue venenatis at. Vivamus ornare orci vel orci sodales maximus. Donec ultricies felis ac nulla fermentum gravida. Phasellus vulputate turpis odio, a varius nibh luctus et. Aliquam tincidunt, metus ut congue porttitor, nibh dui ullamcorper quam, a eleifend elit ipsum sit amet quam. Aenean venenatis mollis interdum. Nunc cursus ex sit amet enim lacinia hendrerit. Nullam at libero iaculis, consectetur velit in, porta sem. Ut mattis ut ex in imperdiet. Maecenas pellentesque sit amet dui eget vehicula. Sed posuere, arcu pretium convallis tincidunt, turpis leo dignissim felis, non euismod diam magna a risus. Suspendisse a arcu nec turpis pulvinar ullamcorper. Nunc iaculis malesuada elit eu pretium. Aenean a neque a sapien tincidunt faucibus.", + "Ut a eleifend augue, eget posuere augue. Proin purus neque, pretium condimentum ipsum ut, venenatis tincidunt nunc. In vitae odio in justo pharetra tincidunt. Maecenas vel tellus interdum, suscipit tellus sit amet, cursus justo. Mauris sollicitudin euismod molestie. Cras eros nisi, molestie vel elementum ut, consequat ac nunc. In consectetur nulla vitae interdum elementum. Praesent faucibus magna et iaculis congue. Curabitur convallis cursus porttitor. Praesent hendrerit justo ut sem convallis sollicitudin eu at odio.", +]; + +static CONTENTS: LazyLock<[Arc; 5]> = LazyLock::new(|| PARAGRAPHS.map(Arc::from)); + +pub fn blog_post(i: u64) -> BlogPost { + BlogPost { + title: format!("Blog post {i}").into(), + content: Arc::clone(&CONTENTS[(i % 5) as usize]), + publication_date: SEPTEMBER_2020 + splitmix64(i) % THIRTY_DAYS, + } +} + +pub fn blog_posts_checksum(n: u64) -> u64 { + (1..=n).map(|i| blog_post(i).checksum()).sum() +} + +/// Like [`blog_posts_checksum`], but also checks that the `i`-th created id returns the `i`-th post. +pub fn blog_posts_by_number_checksum(n: u64) -> u64 { + (1..=n) + .map(|i| mix(i, blog_post(i).checksum())) + .fold(0, u64::wrapping_add) +} diff --git a/rust/src/doublets_impl.rs b/rust/src/doublets_impl.rs deleted file mode 100644 index 458f0e8..0000000 --- a/rust/src/doublets_impl.rs +++ /dev/null @@ -1,492 +0,0 @@ -//! Doublets storage implementation for links - -use crate::{BlogPost, Link, Links, Objects}; -use doublets::{ - mem::{Alloc, FileMapped}, - split::{self, DataPart, IndexPart}, - unit::{self, LinkPart}, - Doublets, DoubletsExt, -}; -use std::{alloc::Global, collections::HashMap, fs::OpenOptions, path::Path}; - -/// Type alias for Doublets united (unit) store with volatile (in-memory) storage. -/// Each link is stored as a contiguous unit containing (id, source, target). -pub type DoubletsUnitedVolatile = unit::Store, Global>>; - -/// Type alias for Doublets split store with volatile (in-memory) storage. -/// Separates data and index into different memory regions for better cache efficiency. -pub type DoubletsSplitVolatile = - split::Store, Global>, Alloc, Global>>; - -/// Type alias for Doublets united (unit) store with non-volatile (file-mapped) storage. -/// Same layout as [`DoubletsUnitedVolatile`], but the links live in a memory-mapped file. -pub type DoubletsUnitedNonVolatile = unit::Store>>; - -/// Type alias for Doublets split store with non-volatile (file-mapped) storage. -/// Same layout as [`DoubletsSplitVolatile`], but data and index live in two memory-mapped files. -pub type DoubletsSplitNonVolatile = - split::Store>, FileMapped>>; - -/// Opens (creating it when missing) a file and maps it as Doublets memory. -pub fn map_file>(path: P) -> FileMapped { - let file = OpenOptions::new() - .read(true) - .write(true) - .create(true) - .open(path) - .expect("Failed to open the links file"); - FileMapped::new(file).expect("Failed to map the links file") -} - -/// Markers of the object like structures stored as links. -/// -/// The markers mirror the ones of the C# `DoubletsDbContext`: a marker for the -/// blog post itself and one marker per property. -#[derive(Debug, Clone, Copy)] -struct Markers { - blog_post: usize, - title: usize, - content: usize, - publication_date_time: usize, - empty_string: usize, -} - -/// State needed to store object like structures as links. -/// -/// Doublets has no notion of a string, so every string is stored as a sequence -/// of links: every distinct character becomes a point link (a "symbol"), and a -/// string becomes a left fold of `(sequence, symbol)` links. Identical strings -/// (and identical prefixes) are therefore stored exactly once, which is how the -/// C# benchmark stores unicode sequences as well. -#[derive(Default)] -struct ObjectsState { - markers: Option, - symbols: HashMap, - characters: HashMap, -} - -impl ObjectsState { - fn clear(&mut self) { - self.markers = None; - self.symbols.clear(); - self.characters.clear(); - } -} - -/// Wrapper to adapt doublets::Doublets to our Links trait -pub struct DoubletsLinks { - store: S, - objects: ObjectsState, -} - -impl DoubletsLinks { - pub fn new(store: S) -> Self { - Self { - store, - objects: ObjectsState::default(), - } - } - - pub fn into_inner(self) -> S { - self.store - } -} - -impl + DoubletsExt> Links for DoubletsLinks { - fn create(&mut self, source: u64, target: u64) -> u64 { - // `create_by` passes its argument as a *restriction* of the query, and - // the memory stores ignore it — it always creates an empty link. The - // source and the target are only assigned by the update that follows, - // which is exactly what `create_link` does (the equivalent of - // `CreateAndUpdate` of the C# `Platform.Data.Doublets`). - self.store - .create_link(source as usize, target as usize) - .expect("Failed to create link") as u64 - } - - fn create_point(&mut self) -> u64 { - self.store.create_point().expect("Failed to create point") as u64 - } - - fn update(&mut self, id: u64, source: u64, target: u64) { - self.store - .update(id as usize, source as usize, target as usize) - .expect("Failed to update link"); - } - - fn delete(&mut self, id: u64) { - self.store - .delete(id as usize) - .expect("Failed to delete link"); - } - - fn delete_all(&mut self) { - // The markers and the symbols are links too, they are gone as well. - self.objects.clear(); - let any = self.store.constants().any; - let ids: Vec = self - .store - .each_iter([any, any, any]) - .map(|link| link.index) - .collect(); - for id in ids { - let _ = self.store.delete(id); - } - } - - fn query_all(&self) -> Vec { - let any = self.store.constants().any; - self.store - .each_iter([any, any, any]) - .map(|link| Link::new(link.index as u64, link.source as u64, link.target as u64)) - .collect() - } - - fn query_by_id(&self, id: u64) -> Option { - self.store - .get_link(id as usize) - .map(|link| Link::new(link.index as u64, link.source as u64, link.target as u64)) - } - - fn query_by_source(&self, source: u64) -> Vec { - let any = self.store.constants().any; - self.store - .each_iter([any, source as usize, any]) - .map(|link| Link::new(link.index as u64, link.source as u64, link.target as u64)) - .collect() - } - - fn query_by_target(&self, target: u64) -> Vec { - let any = self.store.constants().any; - self.store - .each_iter([any, any, target as usize]) - .map(|link| Link::new(link.index as u64, link.source as u64, link.target as u64)) - .collect() - } - - fn query_by_source_target(&self, source: u64, target: u64) -> Vec { - let any = self.store.constants().any; - self.store - .each_iter([any, source as usize, target as usize]) - .map(|link| Link::new(link.index as u64, link.source as u64, link.target as u64)) - .collect() - } - - fn count(&self) -> usize { - self.store.count() - } -} - -impl + DoubletsExt> DoubletsLinks { - /// Returns the markers of the object like structures, creating them on first use. - fn markers(&mut self) -> Markers { - if let Some(markers) = self.objects.markers { - return markers; - } - let markers = Markers { - blog_post: self.point(), - title: self.point(), - content: self.point(), - publication_date_time: self.point(), - empty_string: self.point(), - }; - self.objects.markers = Some(markers); - markers - } - - fn point(&mut self) -> usize { - self.store - .create_point() - .expect("Failed to create a marker") - } - - /// Returns the point link representing `character`, creating it on first use. - fn symbol(&mut self, character: char) -> usize { - if let Some(&symbol) = self.objects.symbols.get(&character) { - return symbol; - } - let symbol = self.point(); - self.objects.symbols.insert(character, symbol); - self.objects.characters.insert(symbol, character); - symbol - } - - /// Stores `text` as a sequence of links and returns the link of the sequence. - fn create_sequence(&mut self, text: &str) -> usize { - let mut sequence: Option = None; - for character in text.chars() { - let symbol = self.symbol(character); - sequence = Some(match sequence { - None => symbol, - Some(previous) => self - .store - .get_or_create(previous, symbol) - .expect("Failed to create a sequence link"), - }); - } - sequence.unwrap_or_else(|| self.markers().empty_string) - } - - /// Restores the string stored as the sequence of links `sequence`. - fn read_sequence(&self, sequence: usize) -> String { - if Some(sequence) == self.objects.markers.map(|markers| markers.empty_string) { - return String::new(); - } - let mut characters = Vec::new(); - let mut current = sequence; - loop { - if let Some(&character) = self.objects.characters.get(¤t) { - characters.push(character); - break; - } - match self.store.get_link(current) { - Some(link) => { - match self.objects.characters.get(&link.target) { - Some(&character) => characters.push(character), - // Not a sequence of this store, stop instead of looping. - None => break, - } - current = link.source; - } - None => break, - } - } - characters.reverse(); - characters.into_iter().collect() - } - - /// Sets the value of a property of an object, as `PropertiesOperator` does in C#. - fn set_property(&mut self, object: usize, marker: usize, value: usize) { - let property = self - .store - .get_or_create(object, marker) - .expect("Failed to create a property link"); - self.store - .get_or_create(property, value) - .expect("Failed to create a property value link"); - } - - /// Returns the value of a property of an object. - fn property(&self, object: usize, marker: usize) -> Option { - let any = self.store.constants().any; - let property = self.store.search(object, marker)?; - self.store - .find([any, property, any]) - .map(|link| link.target) - } -} - -impl + DoubletsExt> Objects for DoubletsLinks { - fn create_posts(&mut self, posts: &[BlogPost]) -> Vec { - let markers = self.markers(); - let mut ids = Vec::with_capacity(posts.len()); - for post in posts { - let title = self.create_sequence(&post.title); - let content = self.create_sequence(&post.content); - let date = self.create_sequence(&post.publication_date_time.to_string()); - - // The blog post link is `(blog post marker, itself)`, exactly like - // `CreateAndUpdate(_blogPostMarker, Constants.Itself)` in C#. - let object = self.store.create().expect("Failed to create a blog post"); - self.store - .update(object, markers.blog_post, object) - .expect("Failed to mark a blog post"); - - self.set_property(object, markers.title, title); - self.set_property(object, markers.content, content); - self.set_property(object, markers.publication_date_time, date); - ids.push(object as u64); - } - ids - } - - fn read_posts(&self) -> Vec { - let markers = match self.objects.markers { - Some(markers) => markers, - None => return Vec::new(), - }; - self.objects_of(markers) - .into_iter() - .map(|object| { - let title = self - .property(object, markers.title) - .map(|value| self.read_sequence(value)) - .unwrap_or_default(); - let content = self - .property(object, markers.content) - .map(|value| self.read_sequence(value)) - .unwrap_or_default(); - let date = self - .property(object, markers.publication_date_time) - .map(|value| self.read_sequence(value)) - .unwrap_or_default(); - BlogPost { - id: object as u64, - title, - content, - publication_date_time: date.parse().unwrap_or_default(), - } - }) - .collect() - } - - fn delete_posts(&mut self) { - let markers = match self.objects.markers { - Some(markers) => markers, - None => return, - }; - let any = self.store.constants().any; - for object in self.objects_of(markers) { - let properties: Vec = self - .store - .each_iter([any, object, any]) - .map(|link| link.index) - .filter(|&index| index != object) - .collect(); - for property in properties { - let values: Vec = self - .store - .each_iter([any, property, any]) - .map(|link| link.index) - .filter(|&index| index != property) - .collect(); - for value in values { - let _ = self.store.delete(value); - } - let _ = self.store.delete(property); - } - let _ = self.store.delete(object); - } - } - - fn count_posts(&self) -> usize { - match self.objects.markers { - Some(markers) => self.objects_of(markers).len(), - None => 0, - } - } -} - -impl + DoubletsExt> DoubletsLinks { - /// Links of all stored blog posts: `(blog post marker, itself)` links. - fn objects_of(&self, markers: Markers) -> Vec { - let any = self.store.constants().any; - self.store - .each_iter([any, markers.blog_post, any]) - .map(|link| link.index) - .filter(|&index| index != markers.blog_post) - .collect() - } -} - -/// Create a new in-memory doublets united store -pub fn create_united_volatile() -> DoubletsLinks { - let mem = Alloc::new(Global); - let store = DoubletsUnitedVolatile::new(mem).expect("Failed to create doublets store"); - DoubletsLinks::new(store) -} - -/// Create a new in-memory doublets split store -pub fn create_split_volatile() -> DoubletsLinks { - let data_mem = Alloc::new(Global); - let index_mem = Alloc::new(Global); - let store = - DoubletsSplitVolatile::new(data_mem, index_mem).expect("Failed to create doublets store"); - DoubletsLinks::new(store) -} - -/// Create a new file-mapped doublets united store -pub fn create_united_non_volatile>( - path: P, -) -> DoubletsLinks { - let mem = map_file(path); - let store = DoubletsUnitedNonVolatile::new(mem).expect("Failed to create doublets store"); - DoubletsLinks::new(store) -} - -/// Create a new file-mapped doublets split store -pub fn create_split_non_volatile, Q: AsRef>( - data_path: P, - index_path: Q, -) -> DoubletsLinks { - let data_mem = map_file(data_path); - let index_mem = map_file(index_path); - let store = DoubletsSplitNonVolatile::new(data_mem, index_mem) - .expect("Failed to create doublets store"); - DoubletsLinks::new(store) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_create_and_query_united() { - let mut db = create_united_volatile(); - let id = db.create_point(); - assert_eq!(id, 1); - - let link = db.query_by_id(id).unwrap(); - assert_eq!(link.source, id); - assert_eq!(link.target, id); - } - - #[test] - fn test_create_and_query_split() { - let mut db = create_split_volatile(); - let id = db.create_point(); - assert_eq!(id, 1); - - let link = db.query_by_id(id).unwrap(); - assert_eq!(link.source, id); - assert_eq!(link.target, id); - } - - /// Regression test: `create` used to call `create_by([source, target])`, - /// which the memory stores read as a restriction and ignore, so every - /// benchmarked link was created empty instead of connecting two links. - #[test] - fn test_create_assigns_source_and_target() { - let mut db = create_united_volatile(); - let first = db.create_point(); - let second = db.create_point(); - let id = db.create(first, second); - - let link = db.query_by_id(id).unwrap(); - assert_eq!(link.source, first); - assert_eq!(link.target, second); - assert_eq!(db.query_by_source(first).len(), 2); - assert_eq!(db.query_by_target(second).len(), 2); - } - - #[test] - fn test_update() { - let mut db = create_united_volatile(); - let id = db.create(1, 2); - db.update(id, 3, 4); - - let link = db.query_by_id(id).unwrap(); - assert_eq!(link.source, 3); - assert_eq!(link.target, 4); - } - - #[test] - fn test_delete() { - let mut db = create_united_volatile(); - let id = db.create_point(); - db.delete(id); - assert!(db.query_by_id(id).is_none()); - } - - #[test] - fn test_query_by_source() { - let mut db = create_united_volatile(); - let id1 = db.create_point(); - let id2 = db.create_point(); - db.update(id1, id1, id2); - db.update(id2, id1, id1); - - let links = db.query_by_source(id1); - assert_eq!(links.len(), 2); - } -} diff --git a/rust/src/exclusive.rs b/rust/src/exclusive.rs deleted file mode 100644 index 727f428..0000000 --- a/rust/src/exclusive.rs +++ /dev/null @@ -1,46 +0,0 @@ -//! Thread-safe exclusive access wrapper - -use std::cell::UnsafeCell; -use std::ops::{Deref, DerefMut}; - -/// A wrapper that provides mutable access through an immutable reference. -/// This is useful for benchmark scenarios where we need interior mutability. -pub struct Exclusive { - inner: UnsafeCell, -} - -impl Exclusive { - /// Creates a new Exclusive wrapper - pub fn new(value: T) -> Self { - Self { - inner: UnsafeCell::new(value), - } - } - - /// Gets a mutable reference to the inner value - /// - /// # Safety - /// The caller must ensure that no other references exist to the inner value. - #[allow(clippy::mut_from_ref)] - pub fn get_mut(&self) -> &mut T { - unsafe { &mut *self.inner.get() } - } -} - -impl Deref for Exclusive { - type Target = T; - - fn deref(&self) -> &Self::Target { - unsafe { &*self.inner.get() } - } -} - -impl DerefMut for Exclusive { - fn deref_mut(&mut self) -> &mut Self::Target { - unsafe { &mut *self.inner.get() } - } -} - -// SAFETY: We ensure exclusive access through the API design -unsafe impl Send for Exclusive {} -unsafe impl Sync for Exclusive {} diff --git a/rust/src/fork.rs b/rust/src/fork.rs deleted file mode 100644 index ba14355..0000000 --- a/rust/src/fork.rs +++ /dev/null @@ -1,40 +0,0 @@ -//! Fork mechanism for isolated benchmark runs - -use crate::Benched; -use std::ops::{Deref, DerefMut}; - -/// A fork that provides an isolated environment for benchmark iterations. -/// When dropped, it calls unfork() to clean up the state. -pub struct Fork<'a, B: Benched> { - benched: &'a mut B, -} - -impl<'a, B: Benched> Fork<'a, B> { - /// Creates a new Fork from a Benched implementation - pub fn new(benched: &'a mut B) -> Self { - Self { benched } - } -} - -impl<'a, B: Benched> Deref for Fork<'a, B> { - type Target = B; - - fn deref(&self) -> &Self::Target { - self.benched - } -} - -impl<'a, B: Benched> DerefMut for Fork<'a, B> { - fn deref_mut(&mut self) -> &mut Self::Target { - self.benched - } -} - -impl<'a, B: Benched> Drop for Fork<'a, B> { - fn drop(&mut self) { - // SAFETY: We ensure the benched is properly cleaned up - unsafe { - self.benched.unfork(); - } - } -} diff --git a/rust/src/harness.rs b/rust/src/harness.rs new file mode 100644 index 0000000..013cc68 --- /dev/null +++ b/rust/src/harness.rs @@ -0,0 +1,535 @@ +//! Runs every storage through the same lifecycle on a fresh store per repetition: +//! a timed batch per operation, each validated against the expected count and checksum. + +use crate::{ + dataset::{self, blog_post, link, mix, scattered}, + links::{DoubletsLinks, Link, LinksStorage, SqliteLinks}, + memory::{Whole, volatile}, + objects::{BlogPostsStorage, DoubletsBlogPosts, SqliteBlogPosts}, +}; +use doublets::{ + data::{LinkReference, LinksConstants}, + mem::FileMapped, + split, unit, +}; +use serde_json::{Value, json}; +use std::{ + fs, + path::{Path, PathBuf}, + time::{Duration, Instant}, +}; + +pub const LINKS_VARIANTS: [&str; 6] = [ + "SQLite_Memory", + "SQLite_File", + "Doublets_United_Volatile", + "Doublets_United_NonVolatile", + "Doublets_Split_Volatile", + "Doublets_Split_NonVolatile", +]; + +pub const OBJECTS_VARIANTS: [&str; 10] = [ + "SQLite_Memory", + "SQLite_File", + "Doublets_United_Volatile_Cached", + "Doublets_United_Volatile_Uncached", + "Doublets_United_NonVolatile_Cached", + "Doublets_United_NonVolatile_Uncached", + "Doublets_Split_Volatile_Cached", + "Doublets_Split_Volatile_Uncached", + "Doublets_Split_NonVolatile_Cached", + "Doublets_Split_NonVolatile_Uncached", +]; + +pub const WARM_UP_SIZE: u64 = 10_000; +pub const WARM_UP_TIME: Duration = Duration::from_secs(1); + +type Timings = Vec<(&'static str, Duration)>; + +/// Count and order-sensitive checksum of the records an operation has seen. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct Tally { + pub count: u64, + pub checksum: u64, +} + +impl Tally { + fn add(&mut self, checksum: u64) { + self.count += 1; + self.checksum = self.checksum.wrapping_add(checksum); + } + + fn add_link(&mut self, link: Link) { + self.add(dataset::link_checksum( + uint(link.id), + uint(link.from), + uint(link.to), + )); + } + + fn of(count: u64, checksum: u64) -> Self { + Self { count, checksum } + } +} + +fn timed( + timings: &mut Timings, + operation: &'static str, + expected: Tally, + work: impl FnOnce() -> Tally, +) { + let start = Instant::now(); + let actual = work(); + timings.push((operation, start.elapsed())); + assert_eq!(actual, expected, "{operation} returned unexpected records"); +} + +fn int(value: u64) -> T { + T::try_from(value).unwrap() +} + +fn uint(value: T) -> u64 { + value.try_into().unwrap() +} + +pub fn links_lifecycle>( + storage: &mut S, + n: u64, + created: &mut dyn FnMut(), +) -> Timings { + let expected = Tally::of(n, dataset::links_checksum(n, false)); + let mut timings = Timings::new(); + + timed(&mut timings, "create", expected, || { + storage.transaction(|links| { + let mut tally = Tally::default(); + for i in 1..=n { + let (from, to) = link(i); + let id = links.create(int(from), int(to)); + tally.add_link(Link { + id, + from: int(from), + to: int(to), + }); + } + tally + }) + }); + created(); + + timed(&mut timings, "query_all", expected, || { + storage.transaction(|links| { + let mut tally = Tally::default(); + links.each(|link| tally.add_link(link)); + tally + }) + }); + timed(&mut timings, "query_by_id", expected, || { + storage.transaction(|links| { + let mut tally = Tally::default(); + for id in scattered(n) { + tally.add_link(links.get(int(id)).unwrap()); + } + tally + }) + }); + timed(&mut timings, "query_by_from_to", expected, || { + storage.transaction(|links| { + let mut tally = Tally::default(); + for i in scattered(n) { + let (from, to) = link(i); + let id = links.search(int(from), int(to)).unwrap(); + tally.add_link(Link { + id, + from: int(from), + to: int(to), + }); + } + tally + }) + }); + timed(&mut timings, "query_by_from", expected, || { + storage.transaction(|links| { + let mut tally = Tally::default(); + for from in scattered(n) { + links.each_with_from(int(from), |link| tally.add_link(link)); + } + tally + }) + }); + timed(&mut timings, "query_by_to", expected, || { + storage.transaction(|links| { + let mut tally = Tally::default(); + for to in scattered(n) { + links.each_with_to(int(to), |link| tally.add_link(link)); + } + tally + }) + }); + + let swapped = Tally::of(n, dataset::links_checksum(n, true)); + timed(&mut timings, "update", swapped, || { + storage.transaction(|links| { + let mut tally = Tally::default(); + for id in scattered(n) { + let (new_to, new_from) = link(id); + links.update(int(id), int(new_from), int(new_to)); + tally.add(dataset::link_checksum(id, new_from, new_to)); + } + tally + }) + }); + let mut stored = Tally::default(); + storage.each(|link| stored.add_link(link)); + assert_eq!(stored, swapped, "update did not swap every link"); + + timed(&mut timings, "delete", Tally::of(n, 0), || { + storage.transaction(|links| { + let mut tally = Tally::default(); + for id in scattered(n) { + links.delete(int(id)); + tally.add(0); + } + tally + }) + }); + assert_eq!(storage.count(), 0, "delete left links behind"); + timings +} + +pub fn objects_lifecycle>( + storage: &mut S, + n: u64, + created: &mut dyn FnMut(), +) -> Timings { + let expected = Tally::of(n, dataset::blog_posts_checksum(n)); + let mut timings = Timings::new(); + let mut ids = Vec::with_capacity(n as usize); + + timed(&mut timings, "create", expected, || { + storage.transaction(|posts| { + let mut tally = Tally::default(); + for i in 1..=n { + let post = blog_post(i); + ids.push(posts.create(&post)); + tally.add(post.checksum()); + } + tally + }) + }); + created(); + + timed(&mut timings, "read_all", expected, || { + storage.transaction(|posts| { + let mut tally = Tally::default(); + posts.each(|_, post| tally.add(post.checksum())); + tally + }) + }); + timed( + &mut timings, + "read_by_id", + Tally::of(n, dataset::blog_posts_by_number_checksum(n)), + || { + storage.transaction(|posts| { + let mut tally = Tally::default(); + for i in scattered(n) { + let post = posts.get(ids[i as usize - 1]).unwrap(); + tally.add(mix(i, post.checksum())); + } + tally + }) + }, + ); + timed(&mut timings, "delete", Tally::of(n, 0), || { + storage.transaction(|posts| { + let mut tally = Tally::default(); + for i in scattered(n) { + posts.delete(ids[i as usize - 1]); + tally.add(0); + } + tally + }) + }); + assert_eq!(storage.count(), 0, "delete left blog posts behind"); + timings +} + +pub struct Measurement { + pub variant: &'static str, + pub file_bytes: Option, + pub operations: Vec<(&'static str, Vec)>, +} + +impl Measurement { + pub fn to_json(&self) -> Value { + let operations: serde_json::Map = self + .operations + .iter() + .map(|(operation, samples)| { + let mut sorted = samples.clone(); + sorted.sort_by(f64::total_cmp); + let middle = sorted.len() / 2; + let median = if sorted.len() % 2 == 0 { + (sorted[middle - 1] + sorted[middle]) / 2.0 + } else { + sorted[middle] + }; + let summary = json!({ + "median_ns": median, + "min_ns": sorted[0], + "max_ns": sorted[sorted.len() - 1], + "samples_ns": samples, + }); + (operation.to_string(), summary) + }) + .collect(); + json!({ "variant": self.variant, "file_bytes": self.file_bytes, "operations": operations }) + } +} + +fn directory_bytes(directory: &Path) -> u64 { + fs::read_dir(directory) + .unwrap() + .map(|entry| entry.unwrap().metadata().unwrap().len()) + .sum() +} + +/// Times `lifecycle` on `repetitions` fresh stores opened in empty directories, +/// after discarded warm-ups that run for at least [`WARM_UP_TIME`], like in C#. +fn measure( + variant: &'static str, + n: u64, + repetitions: usize, + directory: &Path, + open: impl Fn(&Path) -> S, + lifecycle: fn(&mut S, u64, &mut dyn FnMut()) -> Timings, +) -> Measurement { + let run = |size| { + let workspace = directory.join(variant); + fs::create_dir_all(&workspace).unwrap(); + let mut storage = open(&workspace); + let mut file_bytes = 0; + let timings = lifecycle(&mut storage, size, &mut || { + file_bytes = directory_bytes(&workspace) + }); + drop(storage); + fs::remove_dir_all(&workspace).unwrap(); + (timings, file_bytes) + }; + let warm_up = Instant::now(); + run(n.min(WARM_UP_SIZE)); + while warm_up.elapsed() < WARM_UP_TIME { + run(n.min(WARM_UP_SIZE)); + } + let mut measurement = Measurement { + variant, + file_bytes: None, + operations: Vec::new(), + }; + for repetition in 1..=repetitions { + let (timings, file_bytes) = run(n); + eprintln!("{variant} #{repetition}: {}", summary(&timings, n)); + measurement.file_bytes = (file_bytes > 0).then_some(file_bytes); + for (operation, elapsed) in timings { + let ns_per_operation = elapsed.as_nanos() as f64 / n as f64; + match measurement + .operations + .iter_mut() + .find(|(name, _)| *name == operation) + { + Some((_, samples)) => samples.push(ns_per_operation), + None => measurement + .operations + .push((operation, vec![ns_per_operation])), + } + } + } + measurement +} + +fn summary(timings: &Timings, n: u64) -> String { + timings + .iter() + .map(|(operation, elapsed)| { + format!( + "{operation} {:.0} ns/op", + elapsed.as_nanos() as f64 / n as f64 + ) + }) + .collect::>() + .join(", ") +} + +fn file(directory: &Path, name: &str) -> PathBuf { + directory.join(name) +} + +fn mapped

(directory: &Path, name: &str) -> Whole> { + crate::memory::mapped(file(directory, name)) +} + +pub fn measure_links( + variant: &'static str, + n: u64, + repetitions: usize, + directory: &Path, +) -> Measurement { + match variant { + "SQLite_Memory" => measure( + variant, + n, + repetitions, + directory, + |_| SqliteLinks::::in_memory(), + links_lifecycle, + ), + "SQLite_File" => measure( + variant, + n, + repetitions, + directory, + |dir| SqliteLinks::::open(file(dir, "links.db")), + links_lifecycle, + ), + "Doublets_United_Volatile" => measure( + variant, + n, + repetitions, + directory, + |_| DoubletsLinks::new(unit::Store::::new(volatile()).unwrap()), + links_lifecycle, + ), + "Doublets_United_NonVolatile" => measure( + variant, + n, + repetitions, + directory, + |dir| DoubletsLinks::new(unit::Store::::new(mapped(dir, "links.links")).unwrap()), + links_lifecycle, + ), + "Doublets_Split_Volatile" => measure( + variant, + n, + repetitions, + directory, + |_| DoubletsLinks::new(split::Store::::new(volatile(), volatile()).unwrap()), + links_lifecycle, + ), + "Doublets_Split_NonVolatile" => measure( + variant, + n, + repetitions, + directory, + |dir| { + DoubletsLinks::new( + split::Store::::new( + mapped(dir, "data.links"), + mapped(dir, "index.links"), + ) + .unwrap(), + ) + }, + links_lifecycle, + ), + _ => panic!("unknown links variant {variant}"), + } +} + +pub fn measure_objects( + variant: &'static str, + n: u64, + repetitions: usize, + directory: &Path, +) -> Measurement { + let cached = variant.ends_with("_Cached"); + match variant + .trim_end_matches("_Cached") + .trim_end_matches("_Uncached") + { + "SQLite_Memory" => measure( + variant, + n, + repetitions, + directory, + |_| SqliteBlogPosts::::in_memory(), + objects_lifecycle, + ), + "SQLite_File" => measure( + variant, + n, + repetitions, + directory, + |dir| SqliteBlogPosts::::open(file(dir, "blog_posts.db")), + objects_lifecycle, + ), + "Doublets_United_Volatile" => measure( + variant, + n, + repetitions, + directory, + |_| { + DoubletsBlogPosts::new( + unit::Store::::with_constants(volatile(), LinksConstants::external()) + .unwrap(), + cached, + ) + }, + objects_lifecycle, + ), + "Doublets_United_NonVolatile" => measure( + variant, + n, + repetitions, + directory, + |dir| { + DoubletsBlogPosts::new( + unit::Store::::with_constants( + mapped(dir, "links.links"), + LinksConstants::external(), + ) + .unwrap(), + cached, + ) + }, + objects_lifecycle, + ), + "Doublets_Split_Volatile" => measure( + variant, + n, + repetitions, + directory, + |_| { + DoubletsBlogPosts::new( + split::Store::::with_constants( + volatile(), + volatile(), + LinksConstants::external(), + ) + .unwrap(), + cached, + ) + }, + objects_lifecycle, + ), + "Doublets_Split_NonVolatile" => measure( + variant, + n, + repetitions, + directory, + |dir| { + DoubletsBlogPosts::new( + split::Store::::with_constants( + mapped(dir, "data.links"), + mapped(dir, "index.links"), + LinksConstants::external(), + ) + .unwrap(), + cached, + ) + }, + objects_lifecycle, + ), + _ => panic!("unknown objects variant {variant}"), + } +} diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 8db569f..8432de6 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -1,153 +1,5 @@ -//! SQLite vs Doublets benchmark library -//! -//! This library provides implementations for benchmarking SQLite and Doublets -//! storage systems on basic CRUD operations with links. - -#![feature(allocator_api)] - -pub mod benched; -pub mod doublets_impl; -pub mod exclusive; -pub mod fork; +pub mod dataset; +pub mod harness; +pub mod links; +pub mod memory; pub mod objects; -pub mod sqlite_impl; - -pub use benched::Benched; -pub use exclusive::Exclusive; -pub use fork::Fork; -pub use objects::{generate_posts, BlogPost, Objects, BENCHMARK_OBJECT_COUNT}; - -use once_cell::sync::Lazy; -use std::{ - env, - path::PathBuf, - process, - sync::atomic::{AtomicUsize, Ordering}, -}; - -/// Number of links to use for benchmarking -pub static BENCHMARK_LINK_COUNT: Lazy = Lazy::new(|| { - env::var("BENCHMARK_LINK_COUNT") - .ok() - .and_then(|s| s.parse().ok()) - .unwrap_or(1000) -}); - -/// Number of background links to create before benchmarking -pub static BACKGROUND_LINK_COUNT: Lazy = Lazy::new(|| { - env::var("BACKGROUND_LINK_COUNT") - .ok() - .and_then(|s| s.parse().ok()) - .unwrap_or(3000) -}); - -/// Returns a unique path inside the temporary directory of the machine. -/// -/// The non-volatile (file backed) benchmark subjects store their data there, so -/// that parallel runs of the benchmarks never share a file, and so that nothing -/// is left in the working directory. -pub fn temp_path(name: &str) -> PathBuf { - static COUNTER: AtomicUsize = AtomicUsize::new(0); - let unique = COUNTER.fetch_add(1, Ordering::Relaxed); - env::temp_dir().join(format!( - "sqlite-vs-doublets-{}-{}-{}", - process::id(), - unique, - name - )) -} - -/// A link structure representing a doublet (source -> target relationship) -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct Link { - pub id: u64, - pub source: u64, - pub target: u64, -} - -impl Link { - pub fn new(id: u64, source: u64, target: u64) -> Self { - Self { id, source, target } - } -} - -/// Trait for database operations on links -pub trait Links { - /// Create a new link and return its ID - fn create(&mut self, source: u64, target: u64) -> u64; - - /// Create a point link (self-referencing link) - fn create_point(&mut self) -> u64 { - let id = self.create(0, 0); - self.update(id, id, id); - id - } - - /// Update an existing link - fn update(&mut self, id: u64, source: u64, target: u64); - - /// Delete a link by ID - fn delete(&mut self, id: u64); - - /// Delete all links - fn delete_all(&mut self); - - /// Query all links - fn query_all(&self) -> Vec; - - /// Query a link by ID - fn query_by_id(&self, id: u64) -> Option; - - /// Query links by source - fn query_by_source(&self, source: u64) -> Vec; - - /// Query links by target - fn query_by_target(&self, target: u64) -> Vec; - - /// Query links by source and target - fn query_by_source_target(&self, source: u64, target: u64) -> Vec; - - /// Count all links - fn count(&self) -> usize; -} - -/// Creates `count` background point links and returns their identifiers. -/// -/// Background links make every benchmark run against a non-empty storage, so -/// that queries have to search through unrelated data, as they would in a real -/// application. -pub fn fill_background(links: &mut impl Links, count: usize) -> Vec { - (0..count).map(|_| links.create_point()).collect() -} - -/// Creates `count` links between the given background links. -/// -/// Link number `i` connects `background[i % len]` to a background link further -/// down the list, so that every created doublet is unique (Doublets stores -/// every doublet exactly once) while sources and targets are shared by several -/// links — which is what makes the outgoing and incoming queries meaningful. -pub fn fill_benchmarked(links: &mut impl Links, background: &[u64], count: usize) -> Vec { - assert!( - !background.is_empty(), - "background links are required to create benchmarked links" - ); - let len = background.len(); - (0..count) - .map(|index| { - let source = background[index % len]; - let target = background[(index % len + 1 + index / len) % len]; - let id = links.create(source, target); - Link::new(id, source, target) - }) - .collect() -} - -/// Macro for running benchmarks with proper setup and teardown -#[macro_export] -macro_rules! bench { - ($name:expr, $benched:expr, $op:expr) => {{ - use $crate::Benched; - let mut fork = $benched.fork(); - $op(&mut *fork); - }}; -} diff --git a/rust/src/links.rs b/rust/src/links.rs new file mode 100644 index 0000000..62b94cb --- /dev/null +++ b/rust/src/links.rs @@ -0,0 +1,253 @@ +use doublets::{ + Doublets, + data::{Flow, LinkReference}, +}; +use rusqlite::{Connection, OptionalExtension, params}; +use std::{marker::PhantomData, path::Path}; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Link { + pub id: T, + pub from: T, + pub to: T, +} + +pub trait LinksStorage { + fn create(&mut self, from: T, to: T) -> T; + fn update(&mut self, id: T, from: T, to: T); + fn delete(&mut self, id: T); + fn get(&self, id: T) -> Option>; + fn search(&self, from: T, to: T) -> Option; + fn each(&self, visit: impl FnMut(Link)); + fn each_with_from(&self, from: T, visit: impl FnMut(Link)); + fn each_with_to(&self, to: T, visit: impl FnMut(Link)); + fn count(&self) -> u64; + + fn transaction(&mut self, work: impl FnOnce(&mut Self) -> R) -> R { + work(self) + } +} + +pub struct SqliteLinks { + connection: Connection, + id_type: PhantomData, +} + +impl SqliteLinks { + pub fn open(path: impl AsRef) -> Self { + Self::new(Connection::open(path).unwrap()) + } + + pub fn in_memory() -> Self { + Self::new(Connection::open_in_memory().unwrap()) + } + + fn new(connection: Connection) -> Self { + connection + .execute_batch( + r#" + CREATE TABLE links (id INTEGER PRIMARY KEY, "from" INTEGER NOT NULL, "to" INTEGER NOT NULL); + CREATE INDEX links_from_to ON links ("from", "to"); + CREATE INDEX links_to_from ON links ("to", "from"); + "#, + ) + .unwrap(); + Self { + connection, + id_type: PhantomData, + } + } + + fn each_where( + &self, + sql: &str, + parameters: impl rusqlite::Params, + mut visit: impl FnMut(Link), + ) { + let mut statement = self.connection.prepare_cached(sql).unwrap(); + let mut rows = statement.query(parameters).unwrap(); + while let Some(row) = rows.next().unwrap() { + visit(Link { + id: id(row.get(0).unwrap()), + from: id(row.get(1).unwrap()), + to: id(row.get(2).unwrap()), + }); + } + } +} + +fn sql(id: T) -> i64 { + id.try_into().unwrap() +} + +fn id(sql: i64) -> T { + T::try_from(sql).unwrap() +} + +impl LinksStorage for SqliteLinks { + fn create(&mut self, from: T, to: T) -> T { + let mut statement = self + .connection + .prepare_cached(r#"INSERT INTO links ("from", "to") VALUES (?1, ?2)"#) + .unwrap(); + statement.execute(params![sql(from), sql(to)]).unwrap(); + id(self.connection.last_insert_rowid()) + } + + fn update(&mut self, link: T, from: T, to: T) { + let mut statement = self + .connection + .prepare_cached(r#"UPDATE links SET "from" = ?2, "to" = ?3 WHERE id = ?1"#) + .unwrap(); + statement + .execute(params![sql(link), sql(from), sql(to)]) + .unwrap(); + } + + fn delete(&mut self, link: T) { + let mut statement = self + .connection + .prepare_cached("DELETE FROM links WHERE id = ?1") + .unwrap(); + statement.execute([sql(link)]).unwrap(); + } + + fn get(&self, link: T) -> Option> { + let mut statement = self + .connection + .prepare_cached(r#"SELECT "from", "to" FROM links WHERE id = ?1"#) + .unwrap(); + statement + .query_row([sql(link)], |row| { + Ok(Link { + id: link, + from: id(row.get(0)?), + to: id(row.get(1)?), + }) + }) + .optional() + .unwrap() + } + + fn search(&self, from: T, to: T) -> Option { + let mut statement = self + .connection + .prepare_cached(r#"SELECT id FROM links WHERE "from" = ?1 AND "to" = ?2"#) + .unwrap(); + statement + .query_row([sql(from), sql(to)], |row| row.get(0).map(id)) + .optional() + .unwrap() + } + + fn each(&self, visit: impl FnMut(Link)) { + self.each_where(r#"SELECT id, "from", "to" FROM links"#, [], visit); + } + + fn each_with_from(&self, from: T, visit: impl FnMut(Link)) { + self.each_where( + r#"SELECT id, "from", "to" FROM links WHERE "from" = ?1"#, + [sql(from)], + visit, + ); + } + + fn each_with_to(&self, to: T, visit: impl FnMut(Link)) { + self.each_where( + r#"SELECT id, "from", "to" FROM links WHERE "to" = ?1"#, + [sql(to)], + visit, + ); + } + + fn count(&self) -> u64 { + self.connection + .query_row("SELECT COUNT(*) FROM links", [], |row| row.get(0)) + .unwrap() + } + + fn transaction(&mut self, work: impl FnOnce(&mut Self) -> R) -> R { + self.connection.execute_batch("BEGIN IMMEDIATE").unwrap(); + let result = work(self); + self.connection.execute_batch("COMMIT").unwrap(); + result + } +} + +fn link(link: doublets::Link) -> Link { + Link { + id: link.index, + from: link.source, + to: link.target, + } +} + +pub struct DoubletsLinks { + store: D, + id_type: PhantomData, +} + +impl> DoubletsLinks { + pub fn new(store: D) -> Self { + Self { + store, + id_type: PhantomData, + } + } +} + +impl> LinksStorage for DoubletsLinks { + fn create(&mut self, from: T, to: T) -> T { + self.store.create_link(from, to).unwrap() + } + + fn update(&mut self, id: T, from: T, to: T) { + // doublets 0.5.0 split stores lose a link updated to reference itself unless it starts + // from the empty state `create_link` uses, see experiments/split_store_delete + if from == id || to == id { + self.store + .update(id, T::from_byte(0), T::from_byte(0)) + .unwrap(); + } + self.store.update(id, from, to).unwrap(); + } + + fn delete(&mut self, id: T) { + self.store.delete(id).unwrap(); + } + + fn get(&self, id: T) -> Option> { + self.store.get_link(id).map(link) + } + + fn search(&self, from: T, to: T) -> Option { + self.store.search(from, to) + } + + fn each(&self, mut visit: impl FnMut(Link)) { + self.store.each(|found| { + visit(link(found)); + Flow::Continue + }); + } + + fn each_with_from(&self, from: T, mut visit: impl FnMut(Link)) { + let any = self.store.constants().any; + self.store.each_by([any, from, any], |found| { + visit(link(found)); + Flow::Continue + }); + } + + fn each_with_to(&self, to: T, mut visit: impl FnMut(Link)) { + let any = self.store.constants().any; + self.store.each_by([any, any, to], |found| { + visit(link(found)); + Flow::Continue + }); + } + + fn count(&self) -> u64 { + self.store.count().try_into().unwrap() + } +} diff --git a/rust/src/main.rs b/rust/src/main.rs new file mode 100644 index 0000000..529ef4b --- /dev/null +++ b/rust/src/main.rs @@ -0,0 +1,101 @@ +//! Usage: sqlite-vs-doublets <32|64> [--work N | --repetitions N] +//! [--variants A,B] [--directory DIR] [--output FILE] +//! +//! Repetitions default to `work / size` clamped to `1..=MAX_REPETITIONS`, so smaller sizes get more, +//! but not endless, repetitions. Objects get less work, because one blog post takes hundreds of links. + +use doublets::data::LinkReference; +use serde_json::json; +use sqlite_vs_doublets::harness::{self, LINKS_VARIANTS, Measurement, OBJECTS_VARIANTS}; +use std::{collections::HashMap, env, fs, path::Path}; + +const LINKS_WORK: u64 = 3_000_000; +const OBJECTS_WORK: u64 = 500_000; +const MAX_REPETITIONS: u64 = 10; + +fn main() { + let arguments: Vec = env::args().skip(1).collect(); + let [category, bits, size] = [0, 1, 2].map(|index| { + arguments + .get(index) + .expect("missing positional argument") + .as_str() + }); + let options: HashMap<&str, &str> = arguments[3..] + .chunks(2) + .map(|pair| (pair[0].trim_start_matches("--"), pair[1].as_str())) + .collect(); + let size: u64 = size.replace('_', "").parse().unwrap(); + let default_work = if category == "links" { + LINKS_WORK + } else { + OBJECTS_WORK + }; + let work: u64 = options + .get("work") + .map_or(default_work, |work| work.replace('_', "").parse().unwrap()); + let repetitions: usize = options + .get("repetitions") + .map_or((work / size).clamp(1, MAX_REPETITIONS) as usize, |count| { + count.parse().unwrap() + }); + let all_variants: &[&'static str] = if category == "links" { + &LINKS_VARIANTS + } else { + &OBJECTS_VARIANTS + }; + let variants: Vec<&'static str> = match options.get("variants") { + Some(names) => names + .split(',') + .map(|name| { + *all_variants + .iter() + .find(|known| **known == name) + .expect(name) + }) + .collect(), + None => all_variants.to_vec(), + }; + let directory = options + .get("directory") + .map_or_else(env::temp_dir, |directory| directory.into()); + + let measurements: Vec = match bits { + "32" => measure::(category, &variants, size, repetitions, &directory), + "64" => measure::(category, &variants, size, repetitions, &directory), + _ => panic!("bits must be 32 or 64"), + }; + let report = json!({ + "language": "Rust", + "category": category, + "bits": bits.parse::().unwrap(), + "size": size, + "repetitions": repetitions, + "warm_up_size": size.min(harness::WARM_UP_SIZE), + "warm_up_seconds": harness::WARM_UP_TIME.as_secs_f64(), + "sqlite_version": rusqlite::version(), + "results": measurements.iter().map(Measurement::to_json).collect::>(), + }); + let report = serde_json::to_string_pretty(&report).unwrap(); + match options.get("output") { + Some(path) => fs::write(path, report).unwrap(), + None => println!("{report}"), + } +} + +fn measure( + category: &str, + variants: &[&'static str], + size: u64, + repetitions: usize, + directory: &Path, +) -> Vec { + variants + .iter() + .map(|variant| match category { + "links" => harness::measure_links::(variant, size, repetitions, directory), + "objects" => harness::measure_objects::(variant, size, repetitions, directory), + _ => panic!("category must be links or objects"), + }) + .collect() +} diff --git a/rust/src/memory.rs b/rust/src/memory.rs new file mode 100644 index 0000000..3e7c287 --- /dev/null +++ b/rust/src/memory.rs @@ -0,0 +1,64 @@ +//! Works around a `doublets` 0.5.0 and `platform-mem` 0.3.0 mismatch: stores take the slice returned by +//! `RawMem::grow` for their whole memory, but the memories return only the grown part. A fresh store then +//! sees 2^20 − 8,192 = 1,040,384 links of its 2^20 and panics with "index out of bounds" when it creates +//! more, and any later growth would leave it with an empty memory. [`Whole`] returns the whole memory. + +use doublets::mem::{FileMapped, Global, RawMem, Result}; +use std::{mem::MaybeUninit, path::Path}; + +pub struct Whole(pub M); + +impl RawMem for Whole { + type Item = M::Item; + + fn allocated(&self) -> &[Self::Item] { + self.0.allocated() + } + + fn allocated_mut(&mut self) -> &mut [Self::Item] { + self.0.allocated_mut() + } + + unsafe fn grow( + &mut self, + addition: usize, + fill: impl FnOnce(usize, (&mut [Self::Item], &mut [MaybeUninit])), + ) -> Result<&mut [Self::Item]> { + // SAFETY: the caller's contract is passed on unchanged. + unsafe { self.0.grow(addition, fill)? }; + Ok(self.0.allocated_mut()) + } + + fn shrink(&mut self, cap: usize) -> Result<()> { + self.0.shrink(cap) + } + + fn size_hint(&self) -> Option { + self.0.size_hint() + } +} + +/// Memory of the volatile stores. +pub fn volatile() -> Whole> { + Whole(Global::new()) +} + +/// Memory of the non-volatile stores, mapped from `path`. +pub fn mapped(path: impl AsRef) -> Whole> { + Whole(FileMapped::from_path(path).unwrap()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn grow_returns_the_whole_memory() { + let mut memory = Global::::new(); + memory.grow_filled(2, 1).unwrap(); + assert_eq!(memory.grow_filled(3, 2).unwrap(), [2, 2, 2]); + let mut memory = volatile::(); + memory.grow_filled(2, 1).unwrap(); + assert_eq!(memory.grow_filled(3, 2).unwrap(), [1, 1, 2, 2, 2]); + } +} diff --git a/rust/src/objects.rs b/rust/src/objects.rs index 894d3c4..c881aea 100644 --- a/rust/src/objects.rs +++ b/rust/src/objects.rs @@ -1,164 +1,341 @@ -//! Object-like structures (blog posts) shared by both storages. -//! -//! The C# part of this repository compares SQLite and Doublets on an object -//! like structure — a blog post with a title, a content and a publication date. -//! This module provides the same model for the Rust benchmarks, so that both -//! languages measure the same three operations: -//! -//! | Operation | Meaning | -//! |-----------------------|-----------------------------------------------| -//! | `Objects Create List` | save a list of blog posts into empty storage | -//! | `Objects Read List` | read every stored blog post back | -//! | `Objects Delete List` | delete every stored blog post | -//! -//! The generated data matches `csharp/Model/BlogPosts.cs`: the title is -//! `Blog post {n}`, the content is one of five lorem ipsum paragraphs and the -//! publication date is within the last 30 days. Generation is deterministic -//! (a small xorshift generator seeded by [`OBJECT_SEED`]) so that every run and -//! every storage is benchmarked on exactly the same data. - -use once_cell::sync::Lazy; -use std::env; - -/// Number of blog posts used by the object benchmarks. -/// -/// Can be overridden with the `BENCHMARK_OBJECT_COUNT` environment variable, -/// which the CI workflow lowers for pull request runs. -pub static BENCHMARK_OBJECT_COUNT: Lazy = Lazy::new(|| { - env::var("BENCHMARK_OBJECT_COUNT") - .ok() - .and_then(|value| value.parse().ok()) - .unwrap_or(1000) -}); - -/// Seed of the deterministic generator of blog posts. -pub const OBJECT_SEED: u64 = 0x5148_5157_2D31_3235; - -/// Lorem ipsum paragraphs used as blog post contents. -/// -/// The same five paragraphs are used by `csharp/Model/BlogPosts.cs`. -pub const CONTENTS: [&str; 5] = [ - "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis malesuada blandit mauris nec bibendum.", - "Curabitur tincidunt nibh sit amet finibus dictum. Suspendisse aliquet arcu non rutrum ultrices.", - "Donec vitae felis lectus. Aenean velit sapien, porttitor ut feugiat a, consectetur et risus.", - "Aliquam sed egestas felis. Maecenas sollicitudin nisl in sapien posuere vulputate.", - "Ut a eleifend augue, eget posuere augue. Proin purus neque, pretium condimentum ipsum ut.", -]; - -/// An object like structure: a blog post. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct BlogPost { - /// Storage assigned identifier, `0` before the post is saved. - pub id: u64, - /// Unique title of the post. - pub title: String, - /// Body of the post. - pub content: String, - /// Publication date as a Unix timestamp in seconds. - pub publication_date_time: i64, +use crate::dataset::BlogPost; +use doublets::{ + Doublets, + data::{AddrToRaw, Flow, LinkReference, RawToAddr}, +}; +use rusqlite::{Connection, OptionalExtension, params}; +use std::{collections::HashMap, marker::PhantomData, path::Path, sync::Arc}; + +pub trait BlogPostsStorage { + fn create(&mut self, post: &BlogPost) -> T; + fn get(&mut self, id: T) -> Option; + fn each(&mut self, visit: impl FnMut(T, BlogPost)); + fn delete(&mut self, id: T); + fn count(&mut self) -> u64; + + fn transaction(&mut self, work: impl FnOnce(&mut Self) -> R) -> R { + work(self) + } +} + +pub struct SqliteBlogPosts { + connection: Connection, + id_type: PhantomData, } -impl BlogPost { - /// Creates a not yet stored blog post. - pub fn new( - title: impl Into, - content: impl Into, - publication_date_time: i64, - ) -> Self { +impl SqliteBlogPosts { + pub fn open(path: impl AsRef) -> Self { + Self::new(Connection::open(path).unwrap()) + } + + pub fn in_memory() -> Self { + Self::new(Connection::open_in_memory().unwrap()) + } + + fn new(connection: Connection) -> Self { + connection + .execute_batch( + "CREATE TABLE blog_posts (id INTEGER PRIMARY KEY, title TEXT NOT NULL, content TEXT NOT NULL, publication_date INTEGER NOT NULL)", + ) + .unwrap(); Self { - id: 0, - title: title.into(), - content: content.into(), - publication_date_time, + connection, + id_type: PhantomData, } } } -/// Deterministic xorshift64* generator, so every benchmark sees the same data. -struct Generator(u64); - -impl Generator { - fn next(&mut self) -> u64 { - let mut state = self.0; - state ^= state << 13; - state ^= state >> 7; - state ^= state << 17; - self.0 = state; - state - } +fn blog_post(row: &rusqlite::Row<'_>) -> rusqlite::Result { + Ok(BlogPost { + title: row.get(0)?, + content: row.get(1)?, + publication_date: row.get(2)?, + }) } -/// Generates `count` blog posts, mirroring `BlogPosts.GenerateData` of the C# benchmark. -pub fn generate_posts(count: usize) -> Vec { - // A fixed "now" keeps the generated data stable between runs. - const NOW: i64 = 1_700_000_000; - const SECONDS_IN_30_DAYS: i64 = 30 * 24 * 60 * 60; - - let mut generator = Generator(OBJECT_SEED); - (0..count) - .map(|index| { - let content = CONTENTS[(generator.next() % CONTENTS.len() as u64) as usize]; - let age = (generator.next() % SECONDS_IN_30_DAYS as u64) as i64; - BlogPost::new(format!("Blog post {}", index + 1), content, NOW - age) - }) - .collect() +fn sql(id: T) -> i64 { + id.try_into().unwrap() } -/// Storage of object like structures. -/// -/// Implemented natively by every benchmarked backend: SQLite stores blog posts -/// as rows of a table, Doublets stores them as links (with strings represented -/// as sequences of links). -pub trait Objects { - /// Saves every post of the list, returns the assigned identifiers. - fn create_posts(&mut self, posts: &[BlogPost]) -> Vec; +impl BlogPostsStorage for SqliteBlogPosts { + fn create(&mut self, post: &BlogPost) -> T { + let mut statement = self + .connection + .prepare_cached( + "INSERT INTO blog_posts (title, content, publication_date) VALUES (?1, ?2, ?3)", + ) + .unwrap(); + statement + .execute(params![post.title, post.content, post.publication_date]) + .unwrap(); + T::try_from(self.connection.last_insert_rowid()).unwrap() + } + + fn get(&mut self, id: T) -> Option { + let mut statement = self + .connection + .prepare_cached("SELECT title, content, publication_date FROM blog_posts WHERE id = ?1") + .unwrap(); + statement + .query_row([sql(id)], blog_post) + .optional() + .unwrap() + } - /// Reads every stored post back. - fn read_posts(&self) -> Vec; + fn each(&mut self, mut visit: impl FnMut(T, BlogPost)) { + let mut statement = self + .connection + .prepare_cached("SELECT title, content, publication_date, id FROM blog_posts") + .unwrap(); + let mut rows = statement.query([]).unwrap(); + while let Some(row) = rows.next().unwrap() { + visit( + T::try_from(row.get::<_, i64>(3).unwrap()).unwrap(), + blog_post(row).unwrap(), + ); + } + } - /// Deletes every stored post. - fn delete_posts(&mut self); + fn delete(&mut self, id: T) { + let mut statement = self + .connection + .prepare_cached("DELETE FROM blog_posts WHERE id = ?1") + .unwrap(); + statement.execute([sql(id)]).unwrap(); + } + + fn count(&mut self) -> u64 { + self.connection + .query_row("SELECT COUNT(*) FROM blog_posts", [], |row| row.get(0)) + .unwrap() + } - /// Number of stored posts. - fn count_posts(&self) -> usize; + fn transaction(&mut self, work: impl FnOnce(&mut Self) -> R) -> R { + self.connection.execute_batch("BEGIN IMMEDIATE").unwrap(); + let result = work(self); + self.connection.execute_batch("COMMIT").unwrap(); + result + } +} + +/// Stores each blog post as a `(blog_post, itself)` link with `(post, property) -> value` properties, +/// strings as balanced-variant sequences of Unicode symbols and dates as raw numbers, +/// using the same layout as `Platform.Data.Doublets.Sequences` in C#. +pub struct DoubletsBlogPosts { + links: D, + unicode_symbol: T, + unicode_sequence: T, + title: T, + content: T, + publication_date: T, + blog_post: T, + cache: Option>, } -#[cfg(test)] -mod tests { - use super::*; +#[derive(Default)] +struct SequencesCache { + sequences: HashMap, T>, + strings: HashMap>, +} - #[test] - fn generated_data_is_deterministic() { - assert_eq!(generate_posts(16), generate_posts(16)); +impl> DoubletsBlogPosts { + pub fn new(mut links: D, cache_sequences: bool) -> Self { + let meaning_root = links.create_point().unwrap(); + let mut marker = || { + let marker = links.create().unwrap(); + links.update(marker, meaning_root, marker).unwrap() + }; + Self { + unicode_symbol: marker(), + unicode_sequence: marker(), + title: marker(), + content: marker(), + publication_date: marker(), + blog_post: marker(), + cache: cache_sequences.then(SequencesCache::default), + links, + } } - #[test] - fn generated_titles_match_the_csharp_benchmark() { - let posts = generate_posts(3); - assert_eq!(posts[0].title, "Blog post 1"); - assert_eq!(posts[2].title, "Blog post 3"); + fn sequence(&mut self, string: &Arc) -> T { + if let Some(sequence) = self + .cache + .as_ref() + .and_then(|cache| cache.sequences.get(&**string)) + { + return *sequence; + } + if string.is_empty() { + return self.unicode_sequence; + } + let symbols: Vec = string + .encode_utf16() + .map(|char| { + self.links + .get_or_create( + AddrToRaw.convert(T::try_from(char).unwrap()), + self.unicode_symbol, + ) + .unwrap() + }) + .collect(); + let balanced = self.balanced_variant(symbols); + let sequence = self + .links + .get_or_create(balanced, self.unicode_sequence) + .unwrap(); + if let Some(cache) = &mut self.cache { + cache.sequences.insert(Arc::clone(string), sequence); + } + sequence } - #[test] - fn generated_contents_are_taken_from_the_lorem_ipsum_paragraphs() { - for post in generate_posts(64) { - assert!(CONTENTS.contains(&post.content.as_str())); + fn balanced_variant(&mut self, mut layer: Vec) -> T { + while layer.len() > 1 { + layer = layer + .chunks(2) + .map(|pair| match *pair { + [source, target] => self.links.get_or_create(source, target).unwrap(), + [last] => last, + _ => unreachable!(), + }) + .collect(); } + layer[0] } - #[test] - fn generated_dates_are_within_the_last_30_days() { - let posts = generate_posts(64); - let newest = posts - .iter() - .map(|post| post.publication_date_time) - .max() + fn string(&mut self, sequence: T) -> Arc { + if let Some(string) = self + .cache + .as_ref() + .and_then(|cache| cache.strings.get(&sequence)) + { + return Arc::clone(string); + } + if sequence == self.unicode_sequence { + return Arc::default(); + } + let mut utf16 = Vec::new(); + self.walk(self.source(sequence), |symbol| { + utf16.push(RawToAddr.convert(self.source(symbol)).try_into().unwrap()); + }); + let string: Arc = String::from_utf16(&utf16).unwrap().into(); + if let Some(cache) = &mut self.cache { + cache.strings.insert(sequence, Arc::clone(&string)); + } + string + } + + /// Visits the symbols of a balanced sequence from left to right, like `RightSequenceWalker` in C#. + fn walk(&self, sequence: T, mut visit: impl FnMut(T)) { + let is_symbol = |link| self.target(link) == self.unicode_symbol; + let mut stack = Vec::new(); + let mut element = sequence; + if is_symbol(element) { + return visit(element); + } + loop { + if is_symbol(element) { + let Some(pair) = stack.pop() else { break }; + let (source, target) = (self.source(pair), self.target(pair)); + for part in [source, target] { + if is_symbol(part) { + visit(part); + } + } + element = target; + } else { + stack.push(element); + element = self.source(element); + } + } + } + + fn source(&self, link: T) -> T { + self.links.get_link(link).unwrap().source + } + + fn target(&self, link: T) -> T { + self.links.get_link(link).unwrap().target + } + + fn set_property(&mut self, object: T, property: T, value: T) { + let object_property = self.links.get_or_create(object, property).unwrap(); + let any = self.links.constants().any; + self.links + .delete_query_with([any, object_property, any], |_, _| Flow::Continue) .unwrap(); - let oldest = posts - .iter() - .map(|post| post.publication_date_time) - .min() + self.links.get_or_create(object_property, value).unwrap(); + } + + fn property(&self, object: T, property: T) -> Option { + let object_property = self.links.search(object, property)?; + let any = self.links.constants().any; + self.links + .single([any, object_property, any]) + .map(|value| value.target) + } + + fn delete_property(&mut self, object: T, property: T) { + let object_property = self.links.search(object, property).unwrap(); + let any = self.links.constants().any; + let value = self.links.single([any, object_property, any]).unwrap(); + self.links.delete(value.index).unwrap(); + self.links.delete(object_property).unwrap(); + } +} + +impl> BlogPostsStorage for DoubletsBlogPosts { + fn create(&mut self, post: &BlogPost) -> T { + let blog_post = self.links.create().unwrap(); + self.links + .update(blog_post, self.blog_post, blog_post) .unwrap(); - assert!(newest - oldest <= 30 * 24 * 60 * 60); + let title = self.sequence(&post.title); + self.set_property(blog_post, self.title, title); + let content = self.sequence(&post.content); + self.set_property(blog_post, self.content, content); + let publication_date = AddrToRaw.convert(T::try_from(post.publication_date).unwrap()); + self.set_property(blog_post, self.publication_date, publication_date); + blog_post + } + + fn get(&mut self, id: T) -> Option { + let title = self.property(id, self.title)?; + let content = self.property(id, self.content)?; + let publication_date = self.property(id, self.publication_date)?; + Some(BlogPost { + title: self.string(title), + content: self.string(content), + publication_date: RawToAddr.convert(publication_date).try_into().unwrap(), + }) + } + + fn each(&mut self, mut visit: impl FnMut(T, BlogPost)) { + let any = self.links.constants().any; + let mut ids = Vec::new(); + self.links.each_by([any, self.blog_post, any], |post| { + ids.push(post.index); + Flow::Continue + }); + for id in ids { + let post = self.get(id).unwrap(); + visit(id, post); + } + } + + fn delete(&mut self, id: T) { + for property in [self.title, self.content, self.publication_date] { + self.delete_property(id, property); + } + self.links.delete(id).unwrap(); + } + + fn count(&mut self) -> u64 { + let any = self.links.constants().any; + self.links + .count_by([any, self.blog_post, any]) + .try_into() + .unwrap() } } diff --git a/rust/src/sqlite_impl.rs b/rust/src/sqlite_impl.rs deleted file mode 100644 index b4022a0..0000000 --- a/rust/src/sqlite_impl.rs +++ /dev/null @@ -1,339 +0,0 @@ -//! SQLite implementation for links storage - -use crate::{BlogPost, Link, Links, Objects}; -use rusqlite::{params, Connection}; -use std::path::Path; - -/// SQLite-based links storage -pub struct SqliteLinks { - conn: Connection, - next_id: u64, -} - -impl SqliteLinks { - /// Create a new SQLite links storage with in-memory database - pub fn new_memory() -> Self { - let conn = Connection::open_in_memory().expect("Failed to open in-memory SQLite database"); - Self::init(conn) - } - - /// Create a new SQLite links storage with file-based database - pub fn new_file>(path: P) -> Self { - let conn = Connection::open(path).expect("Failed to open SQLite database file"); - Self::init(conn) - } - - fn init(conn: Connection) -> Self { - Self::create_schema(&conn); - - // Get the max ID to continue from - let next_id: u64 = conn - .query_row("SELECT COALESCE(MAX(id), 0) + 1 FROM links", [], |row| { - row.get(0) - }) - .unwrap_or(1); - - Self { conn, next_id } - } - - /// Creates the tables and the indexes used by the benchmarks. - /// - /// `links` stores the link related benchmark data, `blog_posts` stores the - /// object like structures — the same shape the C# benchmark stores through - /// Entity Framework Core (a primary key, a unique title, a content and a - /// publication date). - fn create_schema(conn: &Connection) { - conn.execute_batch( - "CREATE TABLE IF NOT EXISTS links ( - id INTEGER PRIMARY KEY, - source INTEGER NOT NULL, - target INTEGER NOT NULL - ); - CREATE INDEX IF NOT EXISTS idx_source ON links(source); - CREATE INDEX IF NOT EXISTS idx_target ON links(target); - CREATE INDEX IF NOT EXISTS idx_source_target ON links(source, target); - CREATE TABLE IF NOT EXISTS blog_posts ( - id INTEGER PRIMARY KEY, - title TEXT NOT NULL, - content TEXT NOT NULL, - publication_date_time INTEGER NOT NULL - ); - CREATE UNIQUE INDEX IF NOT EXISTS idx_blog_posts_title ON blog_posts(title);", - ) - .expect("Failed to create the benchmark schema"); - } - - /// Drop all tables and recreate them - pub fn reset(&mut self) { - self.conn - .execute_batch( - "DROP TABLE IF EXISTS links; - DROP TABLE IF EXISTS blog_posts;", - ) - .expect("Failed to drop the benchmark tables"); - Self::create_schema(&self.conn); - self.next_id = 1; - } -} - -impl Links for SqliteLinks { - fn create(&mut self, source: u64, target: u64) -> u64 { - let id = self.next_id; - self.conn - .execute( - "INSERT INTO links (id, source, target) VALUES (?1, ?2, ?3)", - params![id as i64, source as i64, target as i64], - ) - .expect("Failed to insert link"); - self.next_id += 1; - id - } - - fn update(&mut self, id: u64, source: u64, target: u64) { - self.conn - .execute( - "UPDATE links SET source = ?1, target = ?2 WHERE id = ?3", - params![source as i64, target as i64, id as i64], - ) - .expect("Failed to update link"); - } - - fn delete(&mut self, id: u64) { - self.conn - .execute("DELETE FROM links WHERE id = ?1", params![id as i64]) - .expect("Failed to delete link"); - } - - fn delete_all(&mut self) { - self.conn - .execute("DELETE FROM links", []) - .expect("Failed to delete all links"); - self.next_id = 1; - } - - fn query_all(&self) -> Vec { - let mut stmt = self - .conn - .prepare("SELECT id, source, target FROM links") - .expect("Failed to prepare query"); - - stmt.query_map([], |row| { - Ok(Link { - id: row.get::<_, i64>(0)? as u64, - source: row.get::<_, i64>(1)? as u64, - target: row.get::<_, i64>(2)? as u64, - }) - }) - .expect("Failed to query links") - .filter_map(|r| r.ok()) - .collect() - } - - fn query_by_id(&self, id: u64) -> Option { - self.conn - .query_row( - "SELECT id, source, target FROM links WHERE id = ?1", - params![id as i64], - |row| { - Ok(Link { - id: row.get::<_, i64>(0)? as u64, - source: row.get::<_, i64>(1)? as u64, - target: row.get::<_, i64>(2)? as u64, - }) - }, - ) - .ok() - } - - fn query_by_source(&self, source: u64) -> Vec { - let mut stmt = self - .conn - .prepare("SELECT id, source, target FROM links WHERE source = ?1") - .expect("Failed to prepare query"); - - stmt.query_map(params![source as i64], |row| { - Ok(Link { - id: row.get::<_, i64>(0)? as u64, - source: row.get::<_, i64>(1)? as u64, - target: row.get::<_, i64>(2)? as u64, - }) - }) - .expect("Failed to query links") - .filter_map(|r| r.ok()) - .collect() - } - - fn query_by_target(&self, target: u64) -> Vec { - let mut stmt = self - .conn - .prepare("SELECT id, source, target FROM links WHERE target = ?1") - .expect("Failed to prepare query"); - - stmt.query_map(params![target as i64], |row| { - Ok(Link { - id: row.get::<_, i64>(0)? as u64, - source: row.get::<_, i64>(1)? as u64, - target: row.get::<_, i64>(2)? as u64, - }) - }) - .expect("Failed to query links") - .filter_map(|r| r.ok()) - .collect() - } - - fn query_by_source_target(&self, source: u64, target: u64) -> Vec { - let mut stmt = self - .conn - .prepare("SELECT id, source, target FROM links WHERE source = ?1 AND target = ?2") - .expect("Failed to prepare query"); - - stmt.query_map(params![source as i64, target as i64], |row| { - Ok(Link { - id: row.get::<_, i64>(0)? as u64, - source: row.get::<_, i64>(1)? as u64, - target: row.get::<_, i64>(2)? as u64, - }) - }) - .expect("Failed to query links") - .filter_map(|r| r.ok()) - .collect() - } - - fn count(&self) -> usize { - self.conn - .query_row("SELECT COUNT(*) FROM links", [], |row| row.get::<_, i64>(0)) - .unwrap_or(0) as usize - } -} - -impl Objects for SqliteLinks { - fn create_posts(&mut self, posts: &[BlogPost]) -> Vec { - let transaction = self - .conn - .unchecked_transaction() - .expect("Failed to start a transaction"); - let mut ids = Vec::with_capacity(posts.len()); - { - let mut statement = transaction - .prepare( - "INSERT INTO blog_posts (title, content, publication_date_time) - VALUES (?1, ?2, ?3)", - ) - .expect("Failed to prepare the blog post insert"); - for post in posts { - statement - .execute(params![ - post.title, - post.content, - post.publication_date_time - ]) - .expect("Failed to insert a blog post"); - ids.push(transaction.last_insert_rowid() as u64); - } - } - transaction.commit().expect("Failed to commit blog posts"); - ids - } - - fn read_posts(&self) -> Vec { - let mut statement = self - .conn - .prepare("SELECT id, title, content, publication_date_time FROM blog_posts") - .expect("Failed to prepare the blog post query"); - - statement - .query_map([], |row| { - Ok(BlogPost { - id: row.get::<_, i64>(0)? as u64, - title: row.get(1)?, - content: row.get(2)?, - publication_date_time: row.get(3)?, - }) - }) - .expect("Failed to query blog posts") - .filter_map(|post| post.ok()) - .collect() - } - - fn delete_posts(&mut self) { - self.conn - .execute("DELETE FROM blog_posts", []) - .expect("Failed to delete blog posts"); - } - - fn count_posts(&self) -> usize { - self.conn - .query_row("SELECT COUNT(*) FROM blog_posts", [], |row| { - row.get::<_, i64>(0) - }) - .unwrap_or(0) as usize - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_create_and_query() { - let mut db = SqliteLinks::new_memory(); - let id = db.create(1, 2); - assert_eq!(id, 1); - - let link = db.query_by_id(id).unwrap(); - assert_eq!(link.source, 1); - assert_eq!(link.target, 2); - } - - #[test] - fn test_create_point() { - let mut db = SqliteLinks::new_memory(); - let id = db.create_point(); - assert_eq!(id, 1); - - let link = db.query_by_id(id).unwrap(); - assert_eq!(link.source, id); - assert_eq!(link.target, id); - } - - #[test] - fn test_update() { - let mut db = SqliteLinks::new_memory(); - let id = db.create(1, 2); - db.update(id, 3, 4); - - let link = db.query_by_id(id).unwrap(); - assert_eq!(link.source, 3); - assert_eq!(link.target, 4); - } - - #[test] - fn test_delete() { - let mut db = SqliteLinks::new_memory(); - let id = db.create(1, 2); - db.delete(id); - assert!(db.query_by_id(id).is_none()); - } - - #[test] - fn test_query_by_source() { - let mut db = SqliteLinks::new_memory(); - db.create(1, 2); - db.create(1, 3); - db.create(2, 3); - - let links = db.query_by_source(1); - assert_eq!(links.len(), 2); - } - - #[test] - fn test_query_by_target() { - let mut db = SqliteLinks::new_memory(); - db.create(1, 3); - db.create(2, 3); - db.create(2, 4); - - let links = db.query_by_target(3); - assert_eq!(links.len(), 2); - } -} diff --git a/rust/test_out.py b/rust/test_out.py deleted file mode 100644 index 0b27574..0000000 --- a/rust/test_out.py +++ /dev/null @@ -1,103 +0,0 @@ -#!/usr/bin/env python3 -"""Regression tests for the Criterion result parser and Rust report CLI.""" - -import os -import tempfile -import unittest - -import out - - -def complete_output(): - lines = ["Gnuplot not found, using plotters backend"] - value = 1_000 - for operation, _label in out.report.OPERATIONS: - for variant, _variant_label, _color in out.report.VARIANTS: - lines.append( - f"test {operation}/{variant}/10 ... bench: {value:,} ns/iter (+/- 10)" - ) - value += 100 - return "\n".join(lines) + "\n" - - -class ParserTests(unittest.TestCase): - def test_parses_every_operation_and_variant(self): - results = out.parse_text(complete_output()) - - self.assertFalse(out.report.missing_measurements(results)) - self.assertEqual(results["create"]["Doublets_United_Volatile"], 1_000) - - def test_accepts_error_text_inside_a_record(self): - results = out.parse_text( - "test create/SQLite_Memory/10 ... Criterion.rs ERROR: stale cache\n" - "bench: 12,345 ns/iter (+/- 10)\n" - ) - - self.assertEqual(results["create"]["SQLite_Memory"], 12_345) - - def test_does_not_borrow_the_next_record_measurement(self): - results = out.parse_text( - "test create/SQLite_Memory/10 ...\n" - "test create/SQLite_File/10 ... bench: 42 ns/iter (+/- 1)\n" - ) - - self.assertNotIn("SQLite_Memory", results["create"]) - self.assertEqual(results["create"]["SQLite_File"], 42) - - -class MainTests(unittest.TestCase): - def setUp(self): - self.temporary = tempfile.TemporaryDirectory() - self.addCleanup(self.temporary.cleanup) - self.input = os.path.join(self.temporary.name, "out.txt") - self.results = os.path.join(self.temporary.name, "results.md") - self.readme = os.path.join(self.temporary.name, "README.md") - - def write(self, path, content): - with open(path, "w", encoding="utf-8") as handle: - handle.write(content) - - def test_generates_results_and_updates_each_readme(self): - self.write(self.input, complete_output()) - self.write( - self.readme, - "# Results\n\n" - f"{out.report.RUST_START_MARKER}\nold\n" - f"{out.report.RUST_END_MARKER}\n", - ) - - result = out.main( - [ - self.input, - "--results", - self.results, - "--readme", - self.readme, - "--output-dir", - self.temporary.name, - ] - ) - - self.assertEqual(result, 0) - with open(self.results, "r", encoding="utf-8") as handle: - self.assertIn("Objects Delete List", handle.read()) - with open(self.readme, "r", encoding="utf-8") as handle: - self.assertIn("Doublets Split NonVolatile", handle.read()) - - def test_rejects_partial_output(self): - self.write( - self.input, - "test create/SQLite_Memory/10 ... bench: 42 ns/iter (+/- 1)\n", - ) - - self.assertEqual(out.main([self.input, "--results", self.results]), 1) - self.assertFalse(os.path.exists(self.results)) - - def test_empty_input_reports_failure(self): - self.write(self.input, "") - - self.assertEqual(out.main([self.input, "--results", self.results]), 1) - - -if __name__ == "__main__": - unittest.main() diff --git a/rust/tests/storages.rs b/rust/tests/storages.rs index 9ec3854..a69e442 100644 --- a/rust/tests/storages.rs +++ b/rust/tests/storages.rs @@ -1,172 +1,204 @@ -//! Behaviour tests shared by every benchmarked storage. -//! -//! The benchmarks only make sense when every variant implements exactly the -//! same semantics, so the same checks are executed against all six subjects: -//! SQLite (in-memory and file based) and Doublets (united and split stores, -//! each of them volatile and non-volatile). - -#![feature(allocator_api)] - -use sqlite_vs_doublets::benched::{ - DoubletsSplitNonVolatileBenched, DoubletsSplitVolatileBenched, - DoubletsUnitedNonVolatileBenched, DoubletsUnitedVolatileBenched, SqliteFileBenched, - SqliteMemoryBenched, +use doublets::{ + data::{LinkReference, LinksConstants}, + mem::Global, + split, unit, }; use sqlite_vs_doublets::{ - fill_background, fill_benchmarked, generate_posts, Benched, Links, Objects, + dataset::{BlogPost, blog_post, link, scattered}, + harness::{self, LINKS_VARIANTS, OBJECTS_VARIANTS}, + links::{DoubletsLinks, Link, LinksStorage, SqliteLinks}, + memory::volatile, + objects::{BlogPostsStorage, DoubletsBlogPosts, SqliteBlogPosts}, }; -use std::ops::DerefMut; - -const BACKGROUND: usize = 16; -const BENCHMARKED: usize = 32; -const POSTS: usize = 8; +use std::collections::HashSet; -/// Checks the link related operations benchmarked by `benches/benchmarks`. -fn check_links(subject: &mut B) -where - B: Benched + DerefMut, - B::Target: Links + Sized, -{ - { - let mut fork = subject.fork(); - let links = &mut **fork; +const SIZE: u64 = 1_000; - let background = fill_background(links, BACKGROUND); - assert_eq!(background.len(), BACKGROUND); - let created = fill_benchmarked(links, &background, BENCHMARKED); - assert_eq!(created.len(), BENCHMARKED); - assert_eq!(links.count(), BACKGROUND + BENCHMARKED); - assert_eq!(links.query_all().len(), BACKGROUND + BENCHMARKED); +#[test] +fn links_are_unique_and_reference_created_ids() { + let links: HashSet<_> = (1..=SIZE).map(link).collect(); + assert_eq!(links.len(), SIZE as usize); + for i in 1..=SIZE { + let (from, to) = link(i); + assert!((1..=i).contains(&from) && (1..=i).contains(&to)); + } +} - // Every created doublet has to be unique, otherwise Doublets would - // return an already existing link instead of creating a new one. - let mut doublets: Vec<(u64, u64)> = created.iter().map(|l| (l.source, l.target)).collect(); - doublets.sort_unstable(); - doublets.dedup(); - assert_eq!(doublets.len(), BENCHMARKED); +#[test] +fn scattered_visits_every_id_once() { + for n in [1, 2, 3, 10, 1_000, 1_024, 65_536] { + let mut ids: Vec = scattered(n).collect(); + ids.sort(); + assert_eq!(ids, (1..=n).collect::>()); + } +} - let sample = created[BENCHMARKED / 2]; - assert_eq!(links.query_by_id(sample.id), Some(sample)); +#[test] +fn every_links_variant_passes_the_lifecycle() { + let directory = tempdir("links"); + for variant in LINKS_VARIANTS { + harness::measure_links::(variant, SIZE, 1, &directory); + harness::measure_links::(variant, SIZE, 1, &directory); + } +} - let all = links.query_all(); - let outgoing = all.iter().filter(|l| l.source == sample.source).count(); - assert_eq!(links.query_by_source(sample.source).len(), outgoing); - let incoming = all.iter().filter(|l| l.target == sample.target).count(); - assert_eq!(links.query_by_target(sample.target).len(), incoming); - let concrete = all - .iter() - .filter(|l| l.source == sample.source && l.target == sample.target) - .count(); - let queried = links.query_by_source_target(sample.source, sample.target); - assert_eq!(queried.len(), concrete); +#[test] +fn every_objects_variant_passes_the_lifecycle() { + let directory = tempdir("objects"); + for variant in OBJECTS_VARIANTS { + harness::measure_objects::(variant, SIZE / 10, 1, &directory); + harness::measure_objects::(variant, SIZE / 10, 1, &directory); + } +} - // Updating to a doublet that is not stored yet, as the update benchmark does. - let unused = links.create_point(); - links.update(sample.id, unused, background[0]); - let updated = links - .query_by_id(sample.id) - .expect("updated link is missing"); - assert_eq!((updated.source, updated.target), (unused, background[0])); +#[test] +fn split_store_keeps_links_updated_to_reference_themselves() { + let mut links = + DoubletsLinks::new(split::Store::::new(Global::new(), Global::new()).unwrap()); + let point = links.create(1, 1); + let link = links.create(point, 2); + links.update(point, point, point); + links.update(link, link, point); + assert_eq!( + links.get(link), + Some(Link { + id: link, + from: link, + to: point + }) + ); + links.delete(link); + links.delete(point); + assert_eq!(links.count(), 0); +} - let before = links.count(); - links.delete(sample.id); - assert_eq!(links.count(), before - 1); - assert!(links.query_by_id(sample.id).is_none()); +/// A fresh store reserves 2^20 links, so this many links make it grow once. +const GROWN: u64 = (1 << 20) + 1; - links.delete_all(); - assert_eq!(links.count(), 0); - assert!(links.query_all().is_empty()); +fn create_until_grown(mut links: impl LinksStorage) { + for i in 1..=GROWN { + let (from, to) = link(i); + links.create(from as u32, to as u32); + } + assert_eq!(links.count(), GROWN); + for i in [1, 1_040_384, 1_040_385, GROWN] { + let (from, to) = link(i); + let (id, from, to) = (i as u32, from as u32, to as u32); + assert_eq!(links.get(id), Some(Link { id, from, to })); } - - // Dropping the fork has to restore the empty state for the next iteration. - assert_eq!(subject.count(), 0); - assert!(subject.query_all().is_empty()); } -/// Checks the object like (blog post) operations benchmarked by `benches/benchmarks/objects`. -fn check_objects(subject: &mut B) -where - B: Benched + DerefMut, - B::Target: Objects + Links + Sized, -{ - let expected = generate_posts(POSTS); - { - let mut fork = subject.fork(); - let storage = &mut **fork; +#[test] +fn stores_keep_their_links_when_their_memory_grows() { + create_until_grown(DoubletsLinks::new( + unit::Store::::new(volatile()).unwrap(), + )); + create_until_grown(DoubletsLinks::new( + split::Store::::new(volatile(), volatile()).unwrap(), + )); +} - let ids = storage.create_posts(&expected); - assert_eq!(ids.len(), POSTS); - assert_eq!(storage.count_posts(), POSTS); +#[test] +#[should_panic(expected = "query_all returned unexpected records")] +fn lifecycle_rejects_a_storage_that_loses_records() { + struct Lossy(SqliteLinks); - let mut read = storage.read_posts(); - read.sort_by(|left, right| left.title.cmp(&right.title)); - let mut expected = expected.clone(); - expected.sort_by(|left, right| left.title.cmp(&right.title)); - assert_eq!(read.len(), expected.len()); - for (read, expected) in read.iter().zip(expected.iter()) { - assert_eq!(read.title, expected.title); - assert_eq!(read.content, expected.content); - assert_eq!(read.publication_date_time, expected.publication_date_time); + impl LinksStorage for Lossy { + fn create(&mut self, from: u32, to: u32) -> u32 { + self.0.create(from, to) + } + fn update(&mut self, id: u32, from: u32, to: u32) { + self.0.update(id, from, to) + } + fn delete(&mut self, id: u32) { + self.0.delete(id) + } + fn get(&self, id: u32) -> Option> { + self.0.get(id) + } + fn search(&self, from: u32, to: u32) -> Option { + self.0.search(from, to) + } + fn each(&self, visit: impl FnMut(Link)) { + self.0.each_with_from(1, visit) + } + fn each_with_from(&self, from: u32, visit: impl FnMut(Link)) { + self.0.each_with_from(from, visit) + } + fn each_with_to(&self, to: u32, visit: impl FnMut(Link)) { + self.0.each_with_to(to, visit) + } + fn count(&self) -> u64 { + self.0.count() } - - storage.delete_posts(); - assert_eq!(storage.count_posts(), 0); - assert!(storage.read_posts().is_empty()); } - assert_eq!(subject.count_posts(), 0); + harness::links_lifecycle(&mut Lossy(SqliteLinks::in_memory()), SIZE, &mut || {}); } -/// Objects and links live in the same storage, they must not disturb each other. -fn check_objects_and_links(subject: &mut B) -where - B: Benched + DerefMut, - B::Target: Objects + Links + Sized, -{ - let mut fork = subject.fork(); - let storage = &mut **fork; - - let background = fill_background(storage, BACKGROUND); - fill_benchmarked(storage, &background, BENCHMARKED); - let posts = generate_posts(POSTS); - storage.create_posts(&posts); - - assert_eq!(storage.count_posts(), POSTS); - assert_eq!(storage.read_posts().len(), POSTS); - - storage.delete_posts(); - assert_eq!(storage.count_posts(), 0); +fn round_trip(mut posts: impl BlogPostsStorage) { + let mut inputs: Vec = (1..=20).map(blog_post).collect(); + inputs.push(BlogPost { + title: "Ünïcödé 🌍 ✓".into(), + content: "a".into(), + publication_date: 0, + }); + inputs.push(BlogPost { + title: "".into(), + content: "ab".into(), + publication_date: u32::MAX as u64 / 2, + }); + let ids: Vec = inputs.iter().map(|post| posts.create(post)).collect(); + for (id, post) in ids.iter().zip(&inputs) { + assert_eq!(posts.get(*id).as_ref(), Some(post)); + } + let mut stored = Vec::new(); + posts.each(|id, post| stored.push((id, post))); + stored.sort_by_key(|(id, _)| *id); + assert_eq!(stored, ids.into_iter().zip(inputs).collect::>()); } -macro_rules! storage_tests { - ($module:ident, $benched:ty) => { - mod $module { - use super::*; - - #[test] - fn links() { - check_links(&mut <$benched>::setup(())); - } - - #[test] - fn objects() { - check_objects(&mut <$benched>::setup(())); - } - - #[test] - fn objects_and_links() { - check_objects_and_links(&mut <$benched>::setup(())); - } - } - }; +#[test] +fn every_objects_storage_returns_the_stored_posts() { + round_trip::(SqliteBlogPosts::in_memory()); + round_trip::(SqliteBlogPosts::in_memory()); + for cached in [false, true] { + round_trip(DoubletsBlogPosts::new( + unit::Store::::with_constants(Global::new(), LinksConstants::external()) + .unwrap(), + cached, + )); + round_trip(DoubletsBlogPosts::new( + unit::Store::::with_constants(Global::new(), LinksConstants::external()) + .unwrap(), + cached, + )); + round_trip(DoubletsBlogPosts::new( + split::Store::::with_constants( + Global::new(), + Global::new(), + LinksConstants::external(), + ) + .unwrap(), + cached, + )); + round_trip(DoubletsBlogPosts::new( + split::Store::::with_constants( + Global::new(), + Global::new(), + LinksConstants::external(), + ) + .unwrap(), + cached, + )); + } } -storage_tests!(sqlite_memory, SqliteMemoryBenched); -storage_tests!(sqlite_file, SqliteFileBenched); -storage_tests!(doublets_united_volatile, DoubletsUnitedVolatileBenched); -storage_tests!( - doublets_united_non_volatile, - DoubletsUnitedNonVolatileBenched -); -storage_tests!(doublets_split_volatile, DoubletsSplitVolatileBenched); -storage_tests!(doublets_split_non_volatile, DoubletsSplitNonVolatileBenched); +fn tempdir(name: &str) -> std::path::PathBuf { + let directory = std::env::temp_dir().join(format!( + "sqlite-vs-doublets-test-{name}-{}", + std::process::id() + )); + std::fs::create_dir_all(&directory).unwrap(); + directory +} diff --git a/scripts/benchmark_report.py b/scripts/benchmark_report.py index a37e98b..434acd3 100644 --- a/scripts/benchmark_report.py +++ b/scripts/benchmark_report.py @@ -1,318 +1,321 @@ #!/usr/bin/env python3 -"""Shared benchmark reporting helpers for the SQLite vs Doublets comparison.""" +"""Turns the benchmark JSON reports into the README results sections and charts.""" -# The language-specific pipelines parse their own benchmark formats, then use -# this module for consistent Markdown tables, speedup annotations, linear and -# logarithmic charts, and in-place result-section updates. The report format -# follows the sibling LinksPlatform database comparisons. +# Usage: benchmark_report.py RESULTS_DIR [--readme README.md] [--readme README.ru.md] [--charts DIR] +# +# Every JSON report (written by `rust` and `csharp` with `--output`) is one table: a language, +# a category (links or objects), an address/id space (32 or 64 bit) and a size, measured on one machine. +# The README section between the markers is regenerated in the hierarchy +# category -> language -> bits -> size, and charts are written per category, language and bits. -import os +import argparse +import json import re -import shutil -from datetime import datetime, timezone - -try: - import matplotlib - - matplotlib.use("Agg") - import matplotlib.pyplot as plt - import numpy as np - - HAS_MATPLOTLIB = True -except ImportError: # pragma: no cover - exercised only without matplotlib - print("Warning: matplotlib/numpy not installed, skipping chart generation") - HAS_MATPLOTLIB = False - -RUST_START_MARKER = "" -RUST_END_MARKER = "" -CSHARP_START_MARKER = "" -CSHARP_END_MARKER = "" - -# Operations shared by every language of this comparison. The first element of -# each pair is the identifier used by the benchmark runner, the second one is -# the label used in reports. -LINK_OPERATIONS = ( - ("create", "Create"), - ("update", "Update"), - ("delete", "Delete"), - ("query_all", "Each All"), - ("query_by_id", "Each Identity"), - ("query_by_source_target", "Each Concrete"), - ("query_by_source", "Each Outgoing"), - ("query_by_target", "Each Incoming"), -) - -# Object-like structures (blog posts), the operations the C# comparison has -# been built around from the beginning. -OBJECT_OPERATIONS = ( - ("objects_create", "Objects Create List"), - ("objects_read", "Objects Read List"), - ("objects_delete", "Objects Delete List"), -) - -OPERATIONS = LINK_OPERATIONS + OBJECT_OPERATIONS - -# Benchmarked backends: identifier, label and chart color. -DOUBLETS_VARIANTS = ( - ("Doublets_United_Volatile", "Doublets United Volatile", "salmon"), - ("Doublets_United_NonVolatile", "Doublets United NonVolatile", "red"), - ("Doublets_Split_Volatile", "Doublets Split Volatile", "lightgreen"), - ("Doublets_Split_NonVolatile", "Doublets Split NonVolatile", "green"), -) - -SQLITE_VARIANTS = ( - ("SQLite_Memory", "SQLite Memory", "lightblue"), - ("SQLite_File", "SQLite File", "royalblue"), -) - -VARIANTS = DOUBLETS_VARIANTS + SQLITE_VARIANTS - -# Doublets cells are annotated relative to the fastest SQLite measurement of -# the same operation, the same way Comparisons.Neo4jVSDoublets annotates -# against the fastest of the two Neo4j modes. -BASELINES = tuple(key for key, _label, _color in SQLITE_VARIANTS) - - -def empty_results(operations=OPERATIONS): - """Build an empty ``{operation: {variant: nanoseconds}}`` mapping.""" - return {op: {} for op, _label in operations} - - -def has_any_results(results): - """Return ``True`` when at least one measurement was parsed.""" - return any(measurements for measurements in results.values()) - - -def missing_measurements(results, operations=OPERATIONS, variants=VARIANTS): - """List expected ``operation/variant`` pairs absent from parsed output.""" - missing = [] - for operation, _operation_label in operations: - measured = results.get(operation, {}) - for variant, _variant_label, _color in variants: - if not measured.get(variant): - missing.append(f"{operation}/{variant}") - return missing - - -def baseline_of(measurements, baselines=BASELINES): - """Fastest baseline (SQLite) measurement of a single operation, 0 if none.""" - values = [measurements.get(key, 0) for key in baselines] - values = [value for value in values if value] - return min(values) if values else 0 - - -def format_speedup(value, baseline): - """Annotate ``value`` with how it compares to the ``baseline`` measurement.""" - if not value: - return "N/A" - if not baseline: - return f"{value}" - if value <= baseline: - return f"{value} ({baseline / value:.1f}x faster)" - return f"{value} ({value / baseline:.1f}x slower)" - - -def format_results_table(results, operations=OPERATIONS, variants=VARIANTS, baselines=BASELINES): - """Render the Markdown results table (all numbers in nanoseconds).""" - labels = [label for _key, label, _color in variants] - cells_by_variant = [] - for key, _label, _color in variants: - column = [] - for op, _op_label in operations: - measurements = results.get(op, {}) - value = measurements.get(key, 0) - if key in baselines: - column.append(str(value) if value else "N/A") - else: - column.append(format_speedup(value, baseline_of(measurements, baselines))) - cells_by_variant.append(column) - - widths = [ - max(len(label), *(len(cell) for cell in column)) - for label, column in zip(labels, cells_by_variant) - ] - operation_width = max(len("Operation"), *(len(label) for _key, label in operations)) - - header = "| " + "Operation".ljust(operation_width) + " | " - header += " | ".join(label.ljust(width) for label, width in zip(labels, widths)) - header += " |" - separator = "|" + "-" * (operation_width + 2) - separator += "".join("|" + "-" * (width + 2) for width in widths) + "|" - - lines = [header, separator] - for index, (_op, op_label) in enumerate(operations): - row = "| " + op_label.ljust(operation_width) + " | " - row += " | ".join( - column[index].ljust(width) for column, width in zip(cells_by_variant, widths) - ) - row += " |" - lines.append(row) - +import statistics +from pathlib import Path + +START_MARKER = "" +END_MARKER = "" +# The wide tables cannot be wrapped, and the hierarchy repeats headings under different parents. +LINT_OFF = "" +LINT_ON = "" + +CATEGORIES = ("links", "objects") +NOISE = 0.05 +LANGUAGES = ("Rust", "C#") +BITS = (32, 64) + +OPERATIONS = { + "links": ( + "create", + "query_all", + "query_by_id", + "query_by_from_to", + "query_by_from", + "query_by_to", + "update", + "delete", + ), + "objects": ("create", "read_all", "read_by_id", "delete"), +} + + +def russian_plural(count, one, few, many): + if count % 10 == 1 and count % 100 != 11: + return one + return few if 2 <= count % 10 <= 4 and not 12 <= count % 100 <= 14 else many + + +TEXT = { + "en": { + "category": { + "links": "Doublets vs SQLite as storage for links", + "objects": "Doublets vs SQLite as storage for objects", + }, + "language": "{language} doublets vs SQLite", + "bits": "{bits} bit address/id space benchmarks", + "size": {"links": "{size} links", "objects": "{size} blog posts"}, + "operations": { + "create": "Create", + "query_all": "Read all", + "query_by_id": "Read by id", + "query_by_from_to": "Search (from, to)", + "query_by_from": "Read by from", + "query_by_to": "Read by to", + "update": "Update", + "delete": "Delete", + "read_all": "Read all", + "read_by_id": "Read by id", + }, + "storage": "Storage", + "file_size": "File size", + "faster": "{ratio}× faster", + "slower": "{ratio}× slower", + "same": "≈ same", + "provenance": "_{repetitions} after a warm-up, median time per operation. SQLite {sqlite}, {machine}, {source}._", + "repetitions": lambda count: f"{count} repetition" + ("" if count == 1 else "s"), + "machine": "unknown machine", + "local": "a local run", + "run": "[GitHub Actions run]({url}) on {date}", + "missing": "_No results yet._", + "chart": "{language} doublets vs SQLite, {bits} bit, {category}", + "nouns": {"links": "links", "objects": "objects"}, + }, + "ru": { + "category": { + "links": "Дуплеты против SQLite как хранилище связей", + "objects": "Дуплеты против SQLite как хранилище объектов", + }, + "language": "Дуплеты на {language} против SQLite", + "bits": "Тесты с {bits}-битным пространством адресов/идентификаторов", + "size": {"links": "{size} связей", "objects": "{size} записей блога"}, + "operations": { + "create": "Создание", + "query_all": "Чтение всех", + "query_by_id": "Чтение по id", + "query_by_from_to": "Поиск (from, to)", + "query_by_from": "Чтение по from", + "query_by_to": "Чтение по to", + "update": "Обновление", + "delete": "Удаление", + "read_all": "Чтение всех", + "read_by_id": "Чтение по id", + }, + "storage": "Хранилище", + "file_size": "Размер файлов", + "faster": "в {ratio}× быстрее", + "slower": "в {ratio}× медленнее", + "same": "≈ так же", + "provenance": "_{repetitions} после прогрева, медианное время одной операции. SQLite {sqlite}, {machine}, {source}._", + "repetitions": lambda count: f"{count} повтор" + russian_plural(count, "", "а", "ов"), + "machine": "неизвестная машина", + "local": "локальный запуск", + "run": "[запуск GitHub Actions]({url}) от {date}", + "missing": "_Результатов пока нет._", + "chart": "Дуплеты на {language} против SQLite, {bits} бит, {category}", + "nouns": {"links": "связи", "objects": "объекты"}, + }, +} + + +def load(directory): + """All reports in `directory`, keyed by (category, language, bits, size).""" + reports = {} + for path in sorted(Path(directory).glob("*.json")): + report = json.loads(path.read_text(encoding="utf-8")) + key = (report["category"], report["language"], report["bits"], report["size"]) + if key in reports: + raise ValueError(f"{path} duplicates the results of {key}") + reports[key] = report + return reports + + +def baseline(variant): + """Doublets are compared with SQLite of the same durability: volatile with memory, non-volatile with file.""" + if variant.startswith("SQLite"): + return None + return "SQLite_File" if "_NonVolatile" in variant else "SQLite_Memory" + + +def significant(value): + """Three significant digits without an exponent: 999.6 is 1000, not 1e+03.""" + return f"{float(f'{value:.3g}'):g}" + + +def duration(nanoseconds): + rounded = float(significant(nanoseconds)) + for unit, scale in (("s", 1e9), ("ms", 1e6), ("µs", 1e3)): + if rounded >= scale: + return f"{significant(rounded / scale)} {unit}" + return f"{significant(rounded)} ns" + + +def size(count): + return f"{count:,}" + + +def file_size(count): + return "—" if count is None else f"{count / 2**20:.1f} MiB" + + +def quartiles(measured): + """The middle half of the samples: unlike the full range, one outlier repetition does not widen it.""" + samples = measured["samples_ns"] + if len(samples) == 1: + return samples[0], samples[0] + first, _, third = statistics.quantiles(samples, n=4, method="inclusive") + return first, third + + +def comparison(measured, reference, text): + """How `measured` compares with `reference`.""" + # Overlapping interquartile ranges and medians within NOISE of each other (single samples have no range) + # are not called a difference. + (low, high), (reference_low, reference_high) = quartiles(measured), quartiles(reference) + overlapping = low <= reference_high and reference_low <= high + ratio = max(measured["median_ns"], reference["median_ns"]) / min(measured["median_ns"], reference["median_ns"]) + if overlapping or ratio < 1 + NOISE: + return text["same"] + if measured["median_ns"] <= reference["median_ns"]: + return text["faster"].format(ratio=significant(reference["median_ns"] / measured["median_ns"])) + return text["slower"].format(ratio=significant(measured["median_ns"] / reference["median_ns"])) + + +def table(report, text): + operations = OPERATIONS[report["category"]] + by_variant = {result["variant"]: result for result in report["results"]} + header = [text["storage"], *(text["operations"][operation] for operation in operations), text["file_size"]] + lines = ["| " + " | ".join(header) + " |", "| --- |" + " ---: |" * (len(header) - 1)] + for variant, result in by_variant.items(): + reference = by_variant.get(baseline(variant)) + cells = [variant.replace("_", " ")] + for operation in operations: + measured = result["operations"][operation] + cell = duration(measured["median_ns"]) + if reference is not None: + cell += f" ({comparison(measured, reference['operations'][operation], text)})" + cells.append(cell) + cells.append(file_size(result["file_bytes"])) + lines.append("| " + " | ".join(cells) + " |") return "\n".join(lines) -def build_provenance( - benchmark_links=None, - background_links=None, - object_count=None, - generated_at=None, - language=None, -): - """Describe how and when the committed results were produced.""" - benchmark_links = benchmark_links or os.environ.get("BENCHMARK_LINK_COUNT", "1000") - background_links = background_links or os.environ.get("BACKGROUND_LINK_COUNT", "3000") - if object_count is not False: - object_count = object_count or os.environ.get("BENCHMARK_OBJECT_COUNT", "1000") - generated_at = generated_at or datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC") - - source = "a local benchmark run" - repository = os.environ.get("GITHUB_REPOSITORY") - run_id = os.environ.get("GITHUB_RUN_ID") - if repository and run_id: - source = ( - f"[GitHub Actions run {run_id}]" - f"(https://github.com/{repository}/actions/runs/{run_id})" - ) - - prefix = f"_Generated {generated_at}" - if language: - prefix += f" for {language}" - quantities = ( - f"{benchmark_links} benchmarked links, " - f"{background_links} background links" +def provenance(report, text): + run = report.get("run_url") + return text["provenance"].format( + repetitions=text["repetitions"](report["repetitions"]), + sqlite=report["sqlite_version"], + machine=report.get("machine") or text["machine"], + source=text["run"].format(url=run, date=report.get("date")) if run else text["local"], ) - if object_count is not False: - quantities += f", {object_count} objects" - return f"{prefix} by {source} — {quantities}._" - -def render_results_section(results, provenance=None, **table_options): - """Render a results section: provenance line plus the results table.""" - provenance = provenance if provenance is not None else build_provenance() - return f"{provenance}\n\n{format_results_table(results, **table_options)}" +def chart_name(category, language, bits): + return f"{category}-{language.lower().replace('#', 'sharp')}-{bits}.png" + + +def section(reports, language_code, charts_link): + """The Markdown results section in the category -> language -> bits -> size hierarchy.""" + text = TEXT[language_code] + lines = [] + for category in CATEGORIES: + lines += [f"## {text['category'][category]}", ""] + for language in LANGUAGES: + lines += [f"### {text['language'].format(language=language)}", ""] + for bits in BITS: + lines += [f"#### {text['bits'].format(bits=bits)}", ""] + sizes = sorted(key[3] for key in reports if key[:3] == (category, language, bits)) + if not sizes: + lines += [text["missing"], ""] + continue + for count in sizes: + report = reports[(category, language, bits, count)] + lines += [ + f"##### {text['size'][category].format(size=size(count))}", + "", + provenance(report, text), + "", + table(report, text), + "", + ] + if charts_link is not None: + name = chart_name(category, language, bits) + title = text["chart"].format(language=language, bits=bits, category=text["nouns"][category]) + lines += [f"![{title}]({charts_link}/{name})", ""] + return "\n".join(lines).rstrip() + "\n" + + +def replace_section(document, generated): + pattern = re.compile(re.escape(START_MARKER) + ".*?" + re.escape(END_MARKER), re.DOTALL) + if not pattern.search(document): + raise ValueError(f"{START_MARKER} ... {END_MARKER} markers are missing") + return pattern.sub(lambda _: f"{START_MARKER}\n{LINT_OFF}\n{generated}{LINT_ON}\n{END_MARKER}", document) + + +def charts(reports, directory): + """One logarithmic bar chart per category, language and bits, with a panel per size.""" + import matplotlib -def update_markers(path, section, start_marker, end_marker): - """Replace a marked Markdown section and report whether it changed.""" - with open(path, "r", encoding="utf-8") as handle: - document = handle.read() - - if start_marker not in document or end_marker not in document: - raise ValueError(f"{path} does not contain the {start_marker} / {end_marker} markers") + matplotlib.use("Agg") + import matplotlib.pyplot as plt - pattern = re.compile( - re.escape(start_marker) + r".*?" + re.escape(end_marker), - re.DOTALL, - ) - replacement = f"{start_marker}\n{section}\n{end_marker}" - updated = pattern.sub(lambda _match: replacement, document, count=1) - - if updated == document: - return False - - with open(path, "w", encoding="utf-8") as handle: - handle.write(updated) - return True - - -def _series(results, variant, operations): - """Measurements of one variant across all operations, 0 when missing.""" - return [results.get(op, {}).get(variant, 0) for op, _label in operations] - - -def _ensure_min_visible(values, minimum): - """Keep non-zero bars at least ``minimum`` wide so they stay visible.""" - return [max(value, minimum) if value > 0 else 0 for value in values] - - -def _plot(results, path, title, log_scale, operations, variants): - positions = np.arange(len(operations)) - width = 0.8 / len(variants) - figure, axes = plt.subplots(figsize=(12, 8)) - - series = {key: _series(results, key, operations) for key, _label, _color in variants} - - if log_scale: - plotted = series - else: - # On a linear scale Doublets bars are invisible next to SQLite, so give - # every non-zero measurement a minimum visible width (~0.5% of the - # maximum), matching the sibling benchmark charts. - all_values = [value for values in series.values() for value in values] - max_value = max(all_values) if all_values else 1 - minimum = max_value * 0.005 - plotted = {key: _ensure_min_visible(values, minimum) for key, values in series.items()} - - offset_base = (len(variants) - 1) / 2 - for index, (key, label, color) in enumerate(variants): - offset = (index - offset_base) * width - axes.barh(positions + offset, plotted[key], width, label=label, color=color) - - axes.set_xlabel("Time (ns) – log scale" if log_scale else "Time (ns)") - axes.set_title(title) - axes.set_yticks(positions) - axes.set_yticklabels([label for _op, label in operations]) - if log_scale: - axes.set_xscale("log") - axes.legend() - figure.tight_layout() - - directory = os.path.dirname(path) - if directory: - os.makedirs(directory, exist_ok=True) - figure.savefig(path) - plt.close(figure) - print(f"Generated {path}") - - -def generate_charts( - results, - prefix, - title, - output_dir="", - operations=OPERATIONS, - variants=VARIANTS, -): - """Generate linear/log charts and return their paths when available.""" - if not HAS_MATPLOTLIB: - return [] - - linear = os.path.join(output_dir, f"{prefix}.png") if output_dir else f"{prefix}.png" - logarithmic = ( - os.path.join(output_dir, f"{prefix}_log_scale.png") - if output_dir - else f"{prefix}_log_scale.png" - ) - _plot(results, linear, title, False, operations, variants) - _plot(results, logarithmic, title, True, operations, variants) - return [linear, logarithmic] - - -def copy_charts(charts, docs_dir): - """Copy generated charts into the documentation directory.""" - if not docs_dir: - return [] - os.makedirs(docs_dir, exist_ok=True) - copied = [] - for chart in charts: - if os.path.exists(chart): - destination = os.path.join(docs_dir, os.path.basename(chart)) - shutil.copyfile(chart, destination) - copied.append(destination) - print(f"Copied {chart} -> {destination}") - return copied - - -def report_input_excerpt(path, lines=20): - """Describe the tail of ``path`` so an unparsable run can be diagnosed.""" - if not os.path.exists(path): - return f"{path} does not exist" - - with open(path, "r", encoding="utf-8") as handle: - content = handle.read() - - if not content.strip(): - return f"{path} is empty" - - tail = content.splitlines()[-lines:] - return "\n".join([f"Last {len(tail)} line(s) of {path}:", *tail]) + directory.mkdir(parents=True, exist_ok=True) + written = [] + for category in CATEGORIES: + operations = OPERATIONS[category] + labels = [TEXT["en"]["operations"][operation] for operation in operations] + for language in LANGUAGES: + for bits in BITS: + sizes = sorted(key[3] for key in reports if key[:3] == (category, language, bits)) + if not sizes: + continue + figure, axes = plt.subplots( + 1, len(sizes), figsize=(6 * len(sizes), 5), squeeze=False, sharey=False + ) + for axis, count in zip(axes[0], sizes): + results = reports[(category, language, bits, count)]["results"] + width = 0.8 / len(results) + for index, result in enumerate(results): + axis.bar( + [position + index * width for position in range(len(operations))], + [result["operations"][operation]["median_ns"] for operation in operations], + width, + label=result["variant"].replace("_", " "), + color=plt.get_cmap("tab10")(index % 10), + ) + axis.set_yscale("log") + axis.set_title(TEXT["en"]["size"][category].format(size=size(count))) + axis.set_ylabel("median ns per operation (log scale)") + axis.set_xticks([position + 0.4 - width / 2 for position in range(len(operations))]) + axis.set_xticklabels(labels, rotation=30, ha="right") + axis.grid(axis="y", which="major", alpha=0.3) + axes[0][0].legend(fontsize="small") + figure.suptitle(TEXT["en"]["chart"].format(language=language, bits=bits, category=category)) + figure.tight_layout() + path = directory / chart_name(category, language, bits) + figure.savefig(path, dpi=80) + plt.close(figure) + written.append(path) + return written + + +def main(arguments=None): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("results", type=Path, help="directory with the JSON reports") + parser.add_argument("--readme", type=Path, action="append", default=[], help="README to update in place") + parser.add_argument("--charts", type=Path, help="directory for the charts, relative to the READMEs") + options = parser.parse_args(arguments) + reports = load(options.results) + if options.charts is not None: + charts(reports, options.charts) + for readme in options.readme: + language_code = "ru" if readme.name.endswith(".ru.md") else "en" + link = None if options.charts is None else options.charts.resolve().relative_to(readme.resolve().parent).as_posix() + document = readme.read_text(encoding="utf-8") + readme.write_text(replace_section(document, section(reports, language_code, link)), encoding="utf-8") + if not options.readme: + print(section(reports, "en", None)) + + +if __name__ == "__main__": + main() diff --git a/scripts/requirements.txt b/scripts/requirements.txt new file mode 100644 index 0000000..71363ac --- /dev/null +++ b/scripts/requirements.txt @@ -0,0 +1 @@ +matplotlib==3.11.2 diff --git a/scripts/test_benchmark_report.py b/scripts/test_benchmark_report.py index 11c170e..080e304 100644 --- a/scripts/test_benchmark_report.py +++ b/scripts/test_benchmark_report.py @@ -1,95 +1,187 @@ #!/usr/bin/env python3 -"""Tests for shared result formatting and Markdown publication helpers.""" +"""Tests for benchmark_report.py: run with `python -m unittest discover -s scripts`.""" -import os +import contextlib +import importlib.util +import io +import json import tempfile import unittest +from pathlib import Path import benchmark_report as report -class ResultTests(unittest.TestCase): - def test_fastest_sqlite_variant_is_the_baseline(self): - self.assertEqual( - report.baseline_of({"SQLite_Memory": 200, "SQLite_File": 100}), 100 - ) - - def test_formats_faster_and_slower_measurements(self): - self.assertEqual(report.format_speedup(100, 1_000), "100 (10.0x faster)") - self.assertEqual(report.format_speedup(1_000, 100), "1000 (10.0x slower)") - - def test_reports_every_missing_measurement(self): - results = report.empty_results([("create", "Create")]) - - missing = report.missing_measurements( - results, - [("create", "Create")], - [("SQLite_Memory", "SQLite Memory", "blue")], - ) - - self.assertEqual(missing, ["create/SQLite_Memory"]) - - -class MarkerTests(unittest.TestCase): +def measurement(median, *samples): + samples = sorted(samples or [median]) + return {"median_ns": median, "min_ns": samples[0], "max_ns": samples[-1], "samples_ns": samples} + + +def results(category, variants, file_bytes=None): + return { + "category": category, + "results": [ + { + "variant": variant, + "file_bytes": file_bytes, + "operations": {operation: measurement(median) for operation in report.OPERATIONS[category]}, + } + for variant, median in variants.items() + ], + } + + +def sample(category="links", language="Rust", bits=64, size=100_000, **extra): + data = results(category, {"SQLite_Memory": 1000, "SQLite_File": 2000, "Doublets_United_Volatile": 100}) + data.update(language=language, bits=bits, size=size, repetitions=3, sqlite_version="3.53.2", **extra) + return data + + +class FormattingTests(unittest.TestCase): + def test_durations_keep_three_significant_digits(self): + self.assertEqual(report.duration(999), "999 ns") + self.assertEqual(report.duration(1_234), "1.23 µs") + self.assertEqual(report.duration(45_600_000), "45.6 ms") + self.assertEqual(report.duration(2e9), "2 s") + + def test_rounding_up_moves_to_the_next_unit_instead_of_an_exponent(self): + # Rust links, 32 bit, 1,000,000 links, read by to: SQLite Memory took 999.6 ns. + self.assertEqual(report.duration(999.6), "1 µs") + self.assertEqual(report.duration(999_700), "1 ms") + self.assertEqual(report.significant(1234.5), "1230") + + def test_sizes(self): + self.assertEqual(report.size(10_000_000), "10,000,000") + self.assertEqual(report.file_size(64 * 2**20), "64.0 MiB") + self.assertEqual(report.file_size(None), "—") + + def test_russian_plurals(self): + repetitions = report.TEXT["ru"]["repetitions"] + self.assertEqual([repetitions(count) for count in (1, 3, 5, 11, 21)], + ["1 повтор", "3 повтора", "5 повторов", "11 повторов", "21 повтор"]) + + +class ComparisonTests(unittest.TestCase): + text = report.TEXT["en"] + + def test_doublets_are_compared_with_sqlite_of_the_same_durability(self): + self.assertEqual(report.baseline("Doublets_Split_Volatile"), "SQLite_Memory") + self.assertEqual(report.baseline("Doublets_United_NonVolatile_Cached"), "SQLite_File") + self.assertIsNone(report.baseline("SQLite_File")) + + def test_clear_differences(self): + self.assertEqual(report.comparison(measurement(100), measurement(1000), self.text), "10× faster") + self.assertEqual(report.comparison(measurement(2500), measurement(1000), self.text), "2.5× slower") + self.assertEqual(report.comparison(measurement(1), measurement(1000), self.text), "1000× faster") + + def test_overlapping_interquartile_ranges_are_not_a_difference(self): + measured, reference = measurement(150, 90, 100, 150, 200, 300), measurement(190, 150, 180, 190, 210, 260) + self.assertEqual(report.quartiles(measured), (100, 200)) + self.assertEqual(report.comparison(measured, reference, self.text), "≈ same") + + def test_outlier_repetitions_do_not_hide_a_difference(self): + # C# objects, 1,000 blog posts, read by id: Doublets Split NonVolatile Cached vs SQLite File (µs). + doublets = measurement(3.75, 2.1, 2.6, 2.7, 3.1, 3.5, 4.0, 5.1, 6.3, 15.7, 23.0) + sqlite = measurement(19.0, 5.7, 11.4, 16.9, 17.6, 18.2, 19.8, 20.6, 28.2, 29.1, 90.9) + self.assertEqual(report.comparison(doublets, sqlite, self.text), "5.07× faster") + + def test_single_samples_within_noise_are_not_a_difference(self): + self.assertEqual(report.comparison(measurement(1040), measurement(1000), self.text), "≈ same") + self.assertEqual(report.comparison(measurement(1100), measurement(1000), self.text), "1.1× slower") + + def test_table_compares_every_operation_with_the_baseline(self): + table = report.table(sample(), self.text).splitlines() + self.assertEqual(len(table), 2 + 3) + self.assertEqual(table[0].count("|"), len(report.OPERATIONS["links"]) + 3) + self.assertEqual(table[1], "| --- |" + " ---: |" * (len(report.OPERATIONS["links"]) + 1)) + self.assertNotIn("faster", table[2]) + self.assertEqual(table[4].count("10× faster"), len(report.OPERATIONS["links"])) + + +class SectionTests(unittest.TestCase): + def test_hierarchy_is_category_language_bits_size(self): + reports = { + ("links", "Rust", 64, 1_000_000): sample(size=1_000_000), + ("links", "Rust", 64, 100_000): sample(), + ("objects", "C#", 32, 100_000): sample("objects", "C#", 32), + } + headings = [line for line in report.section(reports, "en", "docs").splitlines() if line.startswith("#")] + self.assertEqual(headings, [ + "## Doublets vs SQLite as storage for links", + "### Rust doublets vs SQLite", + "#### 32 bit address/id space benchmarks", + "#### 64 bit address/id space benchmarks", + "##### 100,000 links", + "##### 1,000,000 links", + "### C# doublets vs SQLite", + "#### 32 bit address/id space benchmarks", + "#### 64 bit address/id space benchmarks", + "## Doublets vs SQLite as storage for objects", + "### Rust doublets vs SQLite", + "#### 32 bit address/id space benchmarks", + "#### 64 bit address/id space benchmarks", + "### C# doublets vs SQLite", + "#### 32 bit address/id space benchmarks", + "##### 100,000 blog posts", + "#### 64 bit address/id space benchmarks", + ]) + + def test_missing_groups_and_provenance(self): + url = "https://github.com/linksplatform/Comparisons.SQLiteVSDoublets/actions/runs/1" + reports = {("links", "Rust", 64, 100_000): sample(run_url=url, date="2026-10-04", machine="ubuntu-24.04")} + text = report.section(reports, "en", None) + self.assertEqual(text.count("_No results yet._"), 7) + self.assertIn(f"_3 repetitions after a warm-up, median time per operation. SQLite 3.53.2, ubuntu-24.04, " + f"[GitHub Actions run]({url}) on 2026-10-04._", text) + self.assertNotIn("![", text) + + def test_charts_are_linked_per_category_language_and_bits(self): + text = report.section({("objects", "C#", 32, 100_000): sample("objects", "C#", 32)}, "ru", "docs/benchmarks") + self.assertIn("(docs/benchmarks/objects-csharp-32.png)", text) + self.assertIn("## Дуплеты против SQLite как хранилище объектов", text) + self.assertIn("локальный запуск", text) + + +class DocumentTests(unittest.TestCase): def setUp(self): - self.temporary = tempfile.TemporaryDirectory() - self.addCleanup(self.temporary.cleanup) - self.path = os.path.join(self.temporary.name, "README.md") - - def write(self, content): - with open(self.path, "w", encoding="utf-8") as handle: - handle.write(content) - - def read(self): - with open(self.path, "r", encoding="utf-8") as handle: - return handle.read() - - def test_updates_only_the_selected_language(self): - self.write( - f"{report.RUST_START_MARKER}\nold rust\n{report.RUST_END_MARKER}\n" - f"{report.CSHARP_START_MARKER}\nold csharp\n{report.CSHARP_END_MARKER}\n" - ) - - report.update_markers( - self.path, - r"new rust \g<0> $1", - report.RUST_START_MARKER, - report.RUST_END_MARKER, - ) - - self.assertIn(r"new rust \g<0> $1", self.read()) - self.assertIn("old csharp", self.read()) - - def test_update_is_idempotent(self): - self.write( - f"{report.RUST_START_MARKER}\nold\n{report.RUST_END_MARKER}\n" - ) - report.update_markers( - self.path, - "new", - report.RUST_START_MARKER, - report.RUST_END_MARKER, - ) - - self.assertFalse( - report.update_markers( - self.path, - "new", - report.RUST_START_MARKER, - report.RUST_END_MARKER, - ) - ) - - def test_missing_markers_raise(self): - self.write("# No results\n") + temporary = tempfile.TemporaryDirectory() + self.addCleanup(temporary.cleanup) + self.root = Path(temporary.name) + + def test_only_the_marked_section_is_replaced(self): + document = f"before\n{report.START_MARKER}\nold\n{report.END_MARKER}\nafter\n" + self.assertEqual(report.replace_section(document, "new\n"), + f"before\n{report.START_MARKER}\n{report.LINT_OFF}\nnew\n{report.LINT_ON}\n" + f"{report.END_MARKER}\nafter\n") + with self.assertRaises(ValueError): + report.replace_section("no markers", "new\n") + + def test_usage_starts_with_the_summary(self): + with contextlib.redirect_stdout(io.StringIO()) as output, self.assertRaises(SystemExit): + report.main(["--help"]) + self.assertIn("Turns the benchmark JSON reports into the README results sections and charts.", output.getvalue()) + def test_duplicate_reports_are_rejected(self): + for name in ("a.json", "b.json"): + (self.root / name).write_text(json.dumps(sample()), encoding="utf-8") with self.assertRaises(ValueError): - report.update_markers( - self.path, - "new", - report.RUST_START_MARKER, - report.RUST_END_MARKER, - ) + report.load(self.root) + + @unittest.skipUnless(importlib.util.find_spec("matplotlib"), "matplotlib is not installed") + def test_main_updates_readmes_and_writes_charts(self): + results_directory = self.root / "results" + results_directory.mkdir() + for index, data in enumerate((sample(), sample(size=1_000_000), sample("objects", "C#", 32))): + (results_directory / f"{index}.json").write_text(json.dumps(data), encoding="utf-8") + for name in ("README.md", "README.ru.md"): + (self.root / name).write_text(f"intro\n{report.START_MARKER}\n{report.END_MARKER}\n", encoding="utf-8") + report.main([str(results_directory), "--readme", str(self.root / "README.md"), + "--readme", str(self.root / "README.ru.md"), "--charts", str(self.root / "docs")]) + self.assertEqual(sorted(path.name for path in (self.root / "docs").iterdir()), + ["links-rust-64.png", "objects-csharp-32.png"]) + english = (self.root / "README.md").read_text(encoding="utf-8") + self.assertIn("![Rust doublets vs SQLite, 64 bit, links](docs/links-rust-64.png)", english) + self.assertIn("Дуплеты на C# против SQLite", (self.root / "README.ru.md").read_text(encoding="utf-8")) if __name__ == "__main__":