diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..50eb426 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,17 @@ +* text=auto + +*.bat text eol=crlf +*.cmd text eol=crlf +*.sh text eol=lf +gradlew text eol=lf + +*.jar binary +*.zip binary +*.tar binary +*.gz binary + +*.png binary +*.jpg binary +*.jpeg binary +*.gif binary +*.ico binary diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..565e70c --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,29 @@ +name: CI + +on: + pull_request: + push: + branches: + - main + +permissions: + contents: read + +jobs: + check: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Set up JDK + uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: 21 + + - name: Set up Gradle + uses: gradle/actions/setup-gradle@v6 + + - name: Check + run: ./gradlew check diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..9273b77 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,49 @@ +name: Publish + +on: + push: + tags: + - "v*" + workflow_dispatch: + inputs: + version: + description: "Release version, for example 1.0.0" + required: true + type: string + +permissions: + contents: read + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Set release version + shell: bash + run: | + if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then + echo "RELEASE_VERSION=${{ inputs.version }}" >> "$GITHUB_ENV" + else + echo "RELEASE_VERSION=${GITHUB_REF_NAME#v}" >> "$GITHUB_ENV" + fi + + - name: Set up JDK + uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: 21 + + - name: Set up Gradle + uses: gradle/actions/setup-gradle@v6 + + - name: Publish to Maven Central + env: + ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.MAVEN_CENTRAL_USERNAME }} + ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} + ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.SIGNING_IN_MEMORY_KEY }} + ORG_GRADLE_PROJECT_signingInMemoryKeyId: ${{ secrets.SIGNING_IN_MEMORY_KEY_ID }} + ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNING_IN_MEMORY_KEY_PASSWORD }} + run: ./gradlew :mapsmith-core:check :mapsmith-core:publishAndReleaseToMavenCentral diff --git a/.gitignore b/.gitignore index 524f096..5c6c6eb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,24 +1,44 @@ -# Compiled class file -*.class +.idea/ +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ +.kotlin -# Log file -*.log +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ -# BlueJ files -*.ctxt +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ -# Mobile Tools for Java (J2ME) -.mtj.tmp/ +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ -# Package Files # -*.jar -*.war -*.nar -*.ear -*.zip -*.tar.gz -*.rar +### VS Code ### +.vscode/ -# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml -hs_err_pid* -replay_pid* +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/README.md b/README.md index a7acec1..ffe535c 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,152 @@ # mapsmith -A collection of high-performance map implementations for Java + +[![CI](https://github.com/mrk-andreev/mapsmith/actions/workflows/ci.yml/badge.svg)](https://github.com/mrk-andreev/mapsmith/actions/workflows/ci.yml) +[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) + +High-performance primitive map implementations for Java. + +mapsmith focuses on `long`-keyed data structures that avoid key boxing, keep APIs small, and make +specialized access patterns explicit. It currently includes open-addressed hash maps, range maps, +and ranking maps. + +## What's inside + +- `LongLongMap`: a compact primitive map interface for `long` keys and `long` values. +- `LongObjectMap`: a compact map interface for primitive `long` keys and generic object values. +- Open-addressed maps with linear probing, Robin Hood hashing, and SwissTable-style probing. +- Pluggable long hash functions: Murmur3 finalizer, Fibonacci hashing, XOR shift, and identity. +- `LongLongRangeMap`: stores values over long ranges with closed or open bounds. +- `LongObjectRangeMap`: stores object values over long ranges. +- `LongLongRankingMap`: tracks values and returns leaderboard-style ranks. +- JMH benchmarks for comparing map implementations and tuning tradeoffs. + +## Requirements + +- Java 21+ +- Gradle wrapper included in the repository + +## Install + +The core library is published as: + +```kotlin +dependencies { + implementation("name.mrkandreev:mapsmith-core:") +} +``` + +For local development, use the included Gradle wrapper: + +```bash +./gradlew check +``` + +## Quick start + +Create a primitive hash map with a chosen strategy and hashing function: + +```java +import name.mrkandreev.mapsmith.LongLongMap; +import name.mrkandreev.mapsmith.openaddressing.LongHashing; +import name.mrkandreev.mapsmith.openaddressing.LongLongMapFactory; +import name.mrkandreev.mapsmith.openaddressing.MapSpecialization; + +LongLongMap balances = + LongLongMapFactory.create(MapSpecialization.SWISS_TABLE, 1_000, LongHashing.FIBONACCI); + +balances.put(101L, 2_500L); +balances.put(102L, 7_000L); + +long balance = balances.getOrDefault(101L, 0L); +boolean exists = balances.containsKey(102L); +``` + +Use a range map when values apply to spans of keys: + +```java +import name.mrkandreev.mapsmith.range.LongBoundType; +import name.mrkandreev.mapsmith.range.LongLongRangeMap; +import name.mrkandreev.mapsmith.range.TreeLongLongRangeMap; + +LongLongRangeMap tiers = new TreeLongLongRangeMap(); + +tiers.put(0L, 999L, 1L); +tiers.put(1_000L, 4_999L, 2L); +tiers.put(5_000L, LongBoundType.CLOSED, 10_000L, LongBoundType.OPEN, 3L); + +long tier = tiers.getOrDefault(2_500L, -1L); +``` + +Use a ranking map for leaderboard-style ordering. Higher values rank first; equal values are ordered +by key ascending. + +```java +import name.mrkandreev.mapsmith.ranking.LongLongRankingMap; +import name.mrkandreev.mapsmith.ranking.OrderStatisticLongLongMap; + +LongLongRankingMap leaderboard = new OrderStatisticLongLongMap(); + +leaderboard.put(10L, 1_200L); +leaderboard.put(20L, 3_400L); +leaderboard.put(30L, 2_100L); + +int rank = leaderboard.rankOf(20L); +int entriesAfter = leaderboard.countAfter(20L); +``` + +## Modules + +- `mapsmith-core`: library code and tests. +- `mapsmith-samples`: runnable examples for open-addressed maps, custom strategies, range maps, and + ranking maps. +- `mapsmith-benchmarks`: JMH benchmark suite. + +Run the sample app: + +```bash +./gradlew :mapsmith-samples:run +``` + +## Benchmarks + +Run all JMH benchmarks: + +```bash +./gradlew :mapsmith-benchmarks:jmh +``` + +Run one benchmark class: + +```bash +./gradlew :mapsmith-benchmarks:jmh --args='LongLongMapBenchmark' +``` + +Run object-value benchmark classes separately: + +```bash +./gradlew :mapsmith-benchmarks:jmh --args='LongObjectMapBenchmark|LongObjectRangeMapBenchmark' +``` + +Recent local results on an Apple M4 Pro with OpenJDK 25 show the primitive open-addressed maps +outperforming `java.util.HashMap` in the included `getExisting` and `putAll` +benchmarks. The exact winner depends on workload, size, hashing function, and collision profile, so +rerun the JMH suite on your own hardware before making performance-sensitive choices. The captured +benchmark output is available in [mapsmith-benchmarks/results.md](mapsmith-benchmarks/results.md). + +## Development + +Useful commands: + +```bash +./gradlew check +./gradlew spotlessApply +./gradlew :mapsmith-core:test +./gradlew :mapsmith-benchmarks:jmh --args='LongLongRangeMapBenchmark' +``` + +The build uses Java 21 toolchains, JUnit, AssertJ, Spotless, PMD, SpotBugs, Error Prone, JaCoCo, +and JMH. + +## License + +MIT. See [LICENSE](LICENSE). diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..04727b3 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,106 @@ +plugins { + alias(libs.plugins.spotless) + alias(libs.plugins.spotbugs) apply false + alias(libs.plugins.errorprone) apply false + alias(libs.plugins.maven.publish) apply false +} + +group = "name.mrkandreev" + +version = + providers + .gradleProperty("VERSION_NAME") + .orElse(providers.environmentVariable("RELEASE_VERSION")) + .orElse("1.0-SNAPSHOT") + .get() + +repositories { mavenCentral() } + +spotless { + kotlinGradle { + target("*.gradle.kts") + ktfmt(libs.versions.ktfmt.get()) + trimTrailingWhitespace() + endWithNewline() + } +} + +subprojects { + group = rootProject.group + version = rootProject.version + + repositories { mavenCentral() } + + plugins.withType { + apply(plugin = "com.diffplug.spotless") + apply(plugin = "com.github.spotbugs") + apply(plugin = "jacoco") + apply(plugin = "net.ltgt.errorprone") + apply(plugin = "pmd") + + extensions.configure { + toolchain { languageVersion = JavaLanguageVersion.of(21) } + } + + extensions.configure { toolVersion = libs.versions.jacoco.get() } + + extensions.configure { + toolVersion = libs.versions.pmd.get() + isConsoleOutput = true + } + + extensions.configure { + java { + googleJavaFormat() + removeUnusedImports() + trimTrailingWhitespace() + endWithNewline() + } + + kotlinGradle { + target("*.gradle.kts") + ktfmt(libs.versions.ktfmt.get()) + trimTrailingWhitespace() + endWithNewline() + } + } + + dependencies { + add("errorprone", libs.errorprone.core) + add("testImplementation", platform(libs.junit.bom)) + add("testImplementation", libs.junit.jupiter) + add("testImplementation", libs.assertj.core) + add("testRuntimeOnly", libs.junit.platform.launcher) + } + + tasks.withType().configureEach { + options.compilerArgs.add("-XDaddTypeAnnotationsToSymbol=true") + } + + tasks.withType().configureEach { + useJUnitPlatform() + finalizedBy(tasks.named("jacocoTestReport")) + } + + tasks.withType().configureEach { + dependsOn(tasks.withType()) + + reports { + xml.required = true + html.required = true + csv.required = false + } + } + + tasks.withType().configureEach { + excludeFilter = rootProject.layout.projectDirectory.file("config/spotbugs/exclude.xml") + + reports { + create("html") { required = true } + create("xml") { required = false } + } + } + + tasks.named("check") { dependsOn(tasks.named("jacocoTestReport")) } + } +} diff --git a/config/spotbugs/exclude.xml b/config/spotbugs/exclude.xml new file mode 100644 index 0000000..a9918ad --- /dev/null +++ b/config/spotbugs/exclude.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..733cb59 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,2 @@ +org.gradle.jvmargs=-Xmx1g -Dfile.encoding=UTF-8 +org.gradle.parallel=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..78e9a3f --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,27 @@ +[versions] +assertj = "3.27.6" +errorprone = "2.50.0" +errorprone-plugin = "5.1.0" +jacoco = "0.8.15" +jmh = "1.37" +junit = "6.0.0" +ktfmt = "0.61" +maven-publish = "0.36.0" +pmd = "7.25.0" +spotbugs-plugin = "6.5.6" +spotless = "8.6.0" + +[libraries] +assertj-core = { module = "org.assertj:assertj-core", version.ref = "assertj" } +errorprone-core = { module = "com.google.errorprone:error_prone_core", version.ref = "errorprone" } +jmh-core = { module = "org.openjdk.jmh:jmh-core", version.ref = "jmh" } +jmh-generator-annprocess = { module = "org.openjdk.jmh:jmh-generator-annprocess", version.ref = "jmh" } +junit-bom = { module = "org.junit:junit-bom", version.ref = "junit" } +junit-jupiter = { module = "org.junit.jupiter:junit-jupiter" } +junit-platform-launcher = { module = "org.junit.platform:junit-platform-launcher" } + +[plugins] +errorprone = { id = "net.ltgt.errorprone", version.ref = "errorprone-plugin" } +maven-publish = { id = "com.vanniktech.maven.publish", version.ref = "maven-publish" } +spotbugs = { id = "com.github.spotbugs", version.ref = "spotbugs-plugin" } +spotless = { id = "com.diffplug.spotless", version.ref = "spotless" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..249e583 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..79b3ab8 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Thu Jun 11 23:07:33 BST 2026 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.0-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..1b6c787 --- /dev/null +++ b/gradlew @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..107acd3 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/mapsmith-benchmarks/benchmark-viewer.html b/mapsmith-benchmarks/benchmark-viewer.html new file mode 100644 index 0000000..a5b5dc4 --- /dev/null +++ b/mapsmith-benchmarks/benchmark-viewer.html @@ -0,0 +1,663 @@ + + + + + + mapsmith benchmark viewer + + + + +
+
+

mapsmith benchmark viewer

+ +
+
+ +
+
+
+
+

JMH output

+
+
+ + +
+ + +
+ +
+ + +
+
+
+
+ +
+
+
+ +
+
+
+ + + + diff --git a/mapsmith-benchmarks/build.gradle.kts b/mapsmith-benchmarks/build.gradle.kts new file mode 100644 index 0000000..738a1ec --- /dev/null +++ b/mapsmith-benchmarks/build.gradle.kts @@ -0,0 +1,33 @@ +plugins { java } + +val jmh by + sourceSets.creating { + java.srcDir("src/jmh/java") + resources.srcDir("src/jmh/resources") + compileClasspath += sourceSets.main.get().output + configurations.testRuntimeClasspath.get() + runtimeClasspath += output + compileClasspath + } + +configurations[jmh.implementationConfigurationName].extendsFrom( + configurations.testImplementation.get() +) + +configurations[jmh.runtimeOnlyConfigurationName].extendsFrom(configurations.testRuntimeOnly.get()) + +dependencies { + implementation(project(":mapsmith-core")) + + add(jmh.implementationConfigurationName, project(":mapsmith-core")) + add(jmh.implementationConfigurationName, libs.jmh.core) + add(jmh.annotationProcessorConfigurationName, libs.jmh.generator.annprocess) +} + +tasks.register("jmh") { + group = "benchmark" + description = "Runs the JMH benchmarks." + dependsOn(tasks.named(jmh.classesTaskName)) + classpath = jmh.runtimeClasspath + mainClass = "org.openjdk.jmh.Main" +} + +tasks.named("check") { dependsOn(tasks.named(jmh.compileJavaTaskName)) } diff --git a/mapsmith-benchmarks/results.md b/mapsmith-benchmarks/results.md new file mode 100644 index 0000000..46863ee --- /dev/null +++ b/mapsmith-benchmarks/results.md @@ -0,0 +1,99 @@ +# Benchmark results + +Run the `LongLongMapBenchmark` suite with: + +```bash +./gradlew :mapsmith-benchmarks:jmh --args='LongLongMapBenchmark' +``` + +The following results were captured on: + +- OS: macOS 26.5.1 (Darwin 25.5.0), arm64 +- Hardware: MacBook Pro (Mac16,8), Apple M4 Pro +- CPU cores: 12 total (8 performance, 4 efficiency) +- Memory: 48 GB +- JVM: OpenJDK 25 (25+36-3489) +- Gradle: 9.3.0 + +```text +Benchmark (mapKind) (size) Mode Cnt Score Error Units +LongLongMapBenchmark.getExisting LINEAR_PROBING_MURMUR3_FINALIZER 1000 thrpt 10 647570.987 ± 7922.800 ops/s +LongLongMapBenchmark.getExisting LINEAR_PROBING_MURMUR3_FINALIZER 100000 thrpt 10 4091.284 ± 346.016 ops/s +LongLongMapBenchmark.getExisting LINEAR_PROBING_FIBONACCI 1000 thrpt 10 816294.643 ± 12402.125 ops/s +LongLongMapBenchmark.getExisting LINEAR_PROBING_FIBONACCI 100000 thrpt 10 4345.009 ± 608.994 ops/s +LongLongMapBenchmark.getExisting LINEAR_PROBING_XOR_SHIFT 1000 thrpt 10 694567.270 ± 8330.025 ops/s +LongLongMapBenchmark.getExisting LINEAR_PROBING_XOR_SHIFT 100000 thrpt 10 4331.375 ± 586.359 ops/s +LongLongMapBenchmark.getExisting LINEAR_PROBING_IDENTITY 1000 thrpt 10 842079.161 ± 8063.239 ops/s +LongLongMapBenchmark.getExisting LINEAR_PROBING_IDENTITY 100000 thrpt 10 4714.343 ± 319.401 ops/s +LongLongMapBenchmark.getExisting ROBIN_HOOD_MURMUR3_FINALIZER 1000 thrpt 10 578332.739 ± 3500.212 ops/s +LongLongMapBenchmark.getExisting ROBIN_HOOD_MURMUR3_FINALIZER 100000 thrpt 10 3208.451 ± 154.616 ops/s +LongLongMapBenchmark.getExisting ROBIN_HOOD_FIBONACCI 1000 thrpt 10 782382.398 ± 6103.763 ops/s +LongLongMapBenchmark.getExisting ROBIN_HOOD_FIBONACCI 100000 thrpt 10 4265.135 ± 442.401 ops/s +LongLongMapBenchmark.getExisting ROBIN_HOOD_XOR_SHIFT 1000 thrpt 10 650678.108 ± 2764.606 ops/s +LongLongMapBenchmark.getExisting ROBIN_HOOD_XOR_SHIFT 100000 thrpt 10 4209.408 ± 291.070 ops/s +LongLongMapBenchmark.getExisting ROBIN_HOOD_IDENTITY 1000 thrpt 10 942967.963 ± 6203.312 ops/s +LongLongMapBenchmark.getExisting ROBIN_HOOD_IDENTITY 100000 thrpt 10 4685.244 ± 462.282 ops/s +LongLongMapBenchmark.getExisting SWISS_TABLE_MURMUR3_FINALIZER 1000 thrpt 10 614440.865 ± 2317.823 ops/s +LongLongMapBenchmark.getExisting SWISS_TABLE_MURMUR3_FINALIZER 100000 thrpt 10 3898.600 ± 384.452 ops/s +LongLongMapBenchmark.getExisting SWISS_TABLE_FIBONACCI 1000 thrpt 10 789379.420 ± 3260.503 ops/s +LongLongMapBenchmark.getExisting SWISS_TABLE_FIBONACCI 100000 thrpt 10 4641.703 ± 244.405 ops/s +LongLongMapBenchmark.getExisting SWISS_TABLE_XOR_SHIFT 1000 thrpt 10 655457.131 ± 11165.597 ops/s +LongLongMapBenchmark.getExisting SWISS_TABLE_XOR_SHIFT 100000 thrpt 10 4403.082 ± 326.740 ops/s +LongLongMapBenchmark.getExisting SWISS_TABLE_IDENTITY 1000 thrpt 10 821909.278 ± 7632.234 ops/s +LongLongMapBenchmark.getExisting SWISS_TABLE_IDENTITY 100000 thrpt 10 4836.804 ± 248.294 ops/s +LongLongMapBenchmark.getExisting HASH_MAP 1000 thrpt 10 444449.594 ± 16838.833 ops/s +LongLongMapBenchmark.getExisting HASH_MAP 100000 thrpt 10 1999.496 ± 36.114 ops/s +LongLongMapBenchmark.putAll LINEAR_PROBING_MURMUR3_FINALIZER 1000 thrpt 10 388122.930 ± 3063.760 ops/s +LongLongMapBenchmark.putAll LINEAR_PROBING_MURMUR3_FINALIZER 100000 thrpt 10 3267.017 ± 45.871 ops/s +LongLongMapBenchmark.putAll LINEAR_PROBING_FIBONACCI 1000 thrpt 10 447810.987 ± 2366.250 ops/s +LongLongMapBenchmark.putAll LINEAR_PROBING_FIBONACCI 100000 thrpt 10 3266.107 ± 129.783 ops/s +LongLongMapBenchmark.putAll LINEAR_PROBING_XOR_SHIFT 1000 thrpt 10 408573.203 ± 3688.601 ops/s +LongLongMapBenchmark.putAll LINEAR_PROBING_XOR_SHIFT 100000 thrpt 10 3332.231 ± 59.894 ops/s +LongLongMapBenchmark.putAll LINEAR_PROBING_IDENTITY 1000 thrpt 10 467259.529 ± 3640.637 ops/s +LongLongMapBenchmark.putAll LINEAR_PROBING_IDENTITY 100000 thrpt 10 3536.897 ± 39.149 ops/s +LongLongMapBenchmark.putAll ROBIN_HOOD_MURMUR3_FINALIZER 1000 thrpt 10 292037.559 ± 1266.328 ops/s +LongLongMapBenchmark.putAll ROBIN_HOOD_MURMUR3_FINALIZER 100000 thrpt 10 2208.261 ± 203.691 ops/s +LongLongMapBenchmark.putAll ROBIN_HOOD_FIBONACCI 1000 thrpt 10 302963.038 ± 2247.891 ops/s +LongLongMapBenchmark.putAll ROBIN_HOOD_FIBONACCI 100000 thrpt 10 2145.929 ± 216.520 ops/s +LongLongMapBenchmark.putAll ROBIN_HOOD_XOR_SHIFT 1000 thrpt 10 294839.314 ± 581.408 ops/s +LongLongMapBenchmark.putAll ROBIN_HOOD_XOR_SHIFT 100000 thrpt 10 2164.843 ± 80.350 ops/s +LongLongMapBenchmark.putAll ROBIN_HOOD_IDENTITY 1000 thrpt 10 304588.688 ± 1365.613 ops/s +LongLongMapBenchmark.putAll ROBIN_HOOD_IDENTITY 100000 thrpt 10 2333.448 ± 92.305 ops/s +LongLongMapBenchmark.putAll SWISS_TABLE_MURMUR3_FINALIZER 1000 thrpt 10 375817.768 ± 3421.492 ops/s +LongLongMapBenchmark.putAll SWISS_TABLE_MURMUR3_FINALIZER 100000 thrpt 10 3035.654 ± 322.720 ops/s +LongLongMapBenchmark.putAll SWISS_TABLE_FIBONACCI 1000 thrpt 10 439015.150 ± 8078.347 ops/s +LongLongMapBenchmark.putAll SWISS_TABLE_FIBONACCI 100000 thrpt 10 3169.778 ± 131.227 ops/s +LongLongMapBenchmark.putAll SWISS_TABLE_XOR_SHIFT 1000 thrpt 10 404543.853 ± 634.809 ops/s +LongLongMapBenchmark.putAll SWISS_TABLE_XOR_SHIFT 100000 thrpt 10 3423.558 ± 39.249 ops/s +LongLongMapBenchmark.putAll SWISS_TABLE_IDENTITY 1000 thrpt 10 467484.448 ± 2167.180 ops/s +LongLongMapBenchmark.putAll SWISS_TABLE_IDENTITY 100000 thrpt 10 3421.767 ± 149.698 ops/s +LongLongMapBenchmark.putAll HASH_MAP 1000 thrpt 10 138928.555 ± 3809.826 ops/s +LongLongMapBenchmark.putAll HASH_MAP 100000 thrpt 10 839.966 ± 9.800 ops/s + + +Benchmark (mapKind) (size) Mode Cnt Score Error Units +LongLongRangeMapBenchmark.containsExisting RANGE_TREE_MAP 1000 thrpt 10 79429.545 ± 651.073 ops/s +LongLongRangeMapBenchmark.containsExisting RANGE_TREE_MAP 100000 thrpt 10 197.016 ± 4.399 ops/s +LongLongRangeMapBenchmark.containsExisting RANGE_KEY_TREE_MAP 1000 thrpt 10 48644.454 ± 2057.793 ops/s +LongLongRangeMapBenchmark.containsExisting RANGE_KEY_TREE_MAP 100000 thrpt 10 176.682 ± 5.108 ops/s +LongLongRangeMapBenchmark.getExisting RANGE_TREE_MAP 1000 thrpt 10 89626.036 ± 1094.686 ops/s +LongLongRangeMapBenchmark.getExisting RANGE_TREE_MAP 100000 thrpt 10 198.191 ± 0.975 ops/s +LongLongRangeMapBenchmark.getExisting RANGE_KEY_TREE_MAP 1000 thrpt 10 47828.638 ± 1468.857 ops/s +LongLongRangeMapBenchmark.getExisting RANGE_KEY_TREE_MAP 100000 thrpt 10 175.858 ± 3.805 ops/s +LongLongRangeMapBenchmark.getMissing RANGE_TREE_MAP 1000 thrpt 10 83416.467 ± 8685.003 ops/s +LongLongRangeMapBenchmark.getMissing RANGE_TREE_MAP 100000 thrpt 10 192.624 ± 1.155 ops/s +LongLongRangeMapBenchmark.getMissing RANGE_KEY_TREE_MAP 1000 thrpt 10 48122.831 ± 3223.855 ops/s +LongLongRangeMapBenchmark.getMissing RANGE_KEY_TREE_MAP 100000 thrpt 10 175.753 ± 2.325 ops/s +LongLongRangeMapBenchmark.putAll RANGE_TREE_MAP 1000 thrpt 10 14740.120 ± 52.697 ops/s +LongLongRangeMapBenchmark.putAll RANGE_TREE_MAP 100000 thrpt 10 69.584 ± 1.810 ops/s +LongLongRangeMapBenchmark.putAll RANGE_KEY_TREE_MAP 1000 thrpt 10 10032.486 ± 37.437 ops/s +LongLongRangeMapBenchmark.putAll RANGE_KEY_TREE_MAP 100000 thrpt 10 39.236 ± 0.635 ops/s +LongLongRangeMapBenchmark.putCoalescingAll RANGE_TREE_MAP 1000 thrpt 10 9687.511 ± 337.462 ops/s +LongLongRangeMapBenchmark.putCoalescingAll RANGE_TREE_MAP 100000 thrpt 10 43.072 ± 1.630 ops/s +LongLongRangeMapBenchmark.putCoalescingAll RANGE_KEY_TREE_MAP 1000 thrpt 10 6575.522 ± 203.248 ops/s +LongLongRangeMapBenchmark.putCoalescingAll RANGE_KEY_TREE_MAP 100000 thrpt 10 24.329 ± 0.366 ops/s +LongLongRangeMapBenchmark.removeAll RANGE_TREE_MAP 1000 thrpt 10 35105.537 ± 1168.302 ops/s +LongLongRangeMapBenchmark.removeAll RANGE_TREE_MAP 100000 thrpt 10 142.669 ± 2.225 ops/s +LongLongRangeMapBenchmark.removeAll RANGE_KEY_TREE_MAP 1000 thrpt 10 25388.732 ± 751.595 ops/s +LongLongRangeMapBenchmark.removeAll RANGE_KEY_TREE_MAP 100000 thrpt 10 170.237 ± 5.297 ops/s +``` diff --git a/mapsmith-benchmarks/src/jmh/java/name/mrkandreev/mapsmith/benchmarks/LongLongMapBenchmark.java b/mapsmith-benchmarks/src/jmh/java/name/mrkandreev/mapsmith/benchmarks/LongLongMapBenchmark.java new file mode 100644 index 0000000..5583e35 --- /dev/null +++ b/mapsmith-benchmarks/src/jmh/java/name/mrkandreev/mapsmith/benchmarks/LongLongMapBenchmark.java @@ -0,0 +1,194 @@ +package name.mrkandreev.mapsmith.benchmarks; + +import java.util.HashMap; +import java.util.Map; +import java.util.SplittableRandom; +import java.util.concurrent.TimeUnit; +import name.mrkandreev.mapsmith.LongLongMap; +import name.mrkandreev.mapsmith.openaddressing.LongHashing; +import name.mrkandreev.mapsmith.openaddressing.LongLongMapFactory; +import name.mrkandreev.mapsmith.openaddressing.MapSpecialization; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.infra.Blackhole; + +@BenchmarkMode(Mode.Throughput) +@OutputTimeUnit(TimeUnit.SECONDS) +@Warmup(iterations = 5, time = 1) +@Measurement(iterations = 5, time = 1) +@Fork(2) +public class LongLongMapBenchmark { + @Benchmark + public void getExisting(PopulatedMaps maps, Blackhole blackhole) { + blackhole.consume(getExisting(maps.map, maps.lookupKeys)); + } + + @Benchmark + public void putAll(Keys keys, Blackhole blackhole) { + blackhole.consume(putAll(keys)); + } + + private static long getExisting(LongLongMap map, long[] lookupKeys) { + long result = 0L; + for (long key : lookupKeys) { + result += map.get(key); + } + return result; + } + + private static LongLongMap putAll(Keys keys) { + LongLongMap map = keys.mapKind.newMap(keys.size); + for (int i = 0; i < keys.lookupKeys.length; i++) { + map.put(keys.lookupKeys[i], keys.mappedValues[i]); + } + return map; + } + + @State(Scope.Thread) + public static class Keys { + @Param({ + "LINEAR_PROBING_MURMUR3_FINALIZER", + "LINEAR_PROBING_FIBONACCI", + "LINEAR_PROBING_XOR_SHIFT", + "LINEAR_PROBING_IDENTITY", + "ROBIN_HOOD_MURMUR3_FINALIZER", + "ROBIN_HOOD_FIBONACCI", + "ROBIN_HOOD_XOR_SHIFT", + "ROBIN_HOOD_IDENTITY", + "SWISS_TABLE_MURMUR3_FINALIZER", + "SWISS_TABLE_FIBONACCI", + "SWISS_TABLE_XOR_SHIFT", + "SWISS_TABLE_IDENTITY", + "HASH_MAP" + }) + public MapKind mapKind; + + @Param({"1000", "100000"}) + public int size; + + long[] lookupKeys; + long[] mappedValues; + + @Setup(Level.Trial) + public void setUp() { + lookupKeys = new long[size]; + mappedValues = new long[size]; + + fillKeys(lookupKeys, mappedValues); + } + } + + public enum MapKind { + LINEAR_PROBING_MURMUR3_FINALIZER( + MapSpecialization.LINEAR_PROBING, LongHashing.MURMUR3_FINALIZER), + LINEAR_PROBING_FIBONACCI(MapSpecialization.LINEAR_PROBING, LongHashing.FIBONACCI), + LINEAR_PROBING_XOR_SHIFT(MapSpecialization.LINEAR_PROBING, LongHashing.XOR_SHIFT), + LINEAR_PROBING_IDENTITY(MapSpecialization.LINEAR_PROBING, LongHashing.IDENTITY), + ROBIN_HOOD_MURMUR3_FINALIZER(MapSpecialization.ROBIN_HOOD, LongHashing.MURMUR3_FINALIZER), + ROBIN_HOOD_FIBONACCI(MapSpecialization.ROBIN_HOOD, LongHashing.FIBONACCI), + ROBIN_HOOD_XOR_SHIFT(MapSpecialization.ROBIN_HOOD, LongHashing.XOR_SHIFT), + ROBIN_HOOD_IDENTITY(MapSpecialization.ROBIN_HOOD, LongHashing.IDENTITY), + SWISS_TABLE_MURMUR3_FINALIZER(MapSpecialization.SWISS_TABLE, LongHashing.MURMUR3_FINALIZER), + SWISS_TABLE_FIBONACCI(MapSpecialization.SWISS_TABLE, LongHashing.FIBONACCI), + SWISS_TABLE_XOR_SHIFT(MapSpecialization.SWISS_TABLE, LongHashing.XOR_SHIFT), + SWISS_TABLE_IDENTITY(MapSpecialization.SWISS_TABLE, LongHashing.IDENTITY), + HASH_MAP(null, null); + + private final MapSpecialization mapSpecialization; + private final LongHashing hashing; + + MapKind(MapSpecialization specialization, LongHashing hashing) { + mapSpecialization = specialization; + this.hashing = hashing; + } + + private LongLongMap newMap(int expectedSize) { + if (this == HASH_MAP) { + return new HashMapLongLongMap(expectedSize); + } + return LongLongMapFactory.create(mapSpecialization, expectedSize, hashing); + } + } + + @State(Scope.Thread) + public static class PopulatedMaps extends Keys { + LongLongMap map; + + @Setup(Level.Trial) + @Override + public void setUp() { + super.setUp(); + + map = mapKind.newMap(size); + + for (int i = 0; i < lookupKeys.length; i++) { + map.put(lookupKeys[i], mappedValues[i]); + } + } + } + + private record HashMapLongLongMap(Map delegate) implements LongLongMap { + private HashMapLongLongMap(int delegate) { + this(new HashMap<>(hashMapCapacity(delegate))); + } + + @Override + public int size() { + return delegate.size(); + } + + @Override + public boolean containsKey(long key) { + return delegate.containsKey(key); + } + + @Override + public long get(long key) { + return getOrDefault(key, 0L); + } + + @Override + public long getOrDefault(long key, long defaultValue) { + return delegate.getOrDefault(key, defaultValue); + } + + @Override + public long put(long key, long value) { + Long previousValue = delegate.put(key, value); + return previousValue == null ? 0L : previousValue; + } + + @Override + public long remove(long key) { + Long previousValue = delegate.remove(key); + return previousValue == null ? 0L : previousValue; + } + + @Override + public void clear() { + delegate.clear(); + } + + private static int hashMapCapacity(int size) { + return (int) (size / 0.75f) + 1; + } + } + + private static void fillKeys(long[] lookupKeys, long[] mappedValues) { + SplittableRandom random = new SplittableRandom(0x4d6170736d697468L); + for (int i = 0; i < lookupKeys.length; i++) { + lookupKeys[i] = random.nextLong(); + mappedValues[i] = i + 1L; + } + } +} diff --git a/mapsmith-benchmarks/src/jmh/java/name/mrkandreev/mapsmith/benchmarks/LongLongRangeMapBenchmark.java b/mapsmith-benchmarks/src/jmh/java/name/mrkandreev/mapsmith/benchmarks/LongLongRangeMapBenchmark.java new file mode 100644 index 0000000..ad5d3dc --- /dev/null +++ b/mapsmith-benchmarks/src/jmh/java/name/mrkandreev/mapsmith/benchmarks/LongLongRangeMapBenchmark.java @@ -0,0 +1,426 @@ +package name.mrkandreev.mapsmith.benchmarks; + +import java.util.Comparator; +import java.util.NavigableMap; +import java.util.SplittableRandom; +import java.util.TreeMap; +import java.util.concurrent.TimeUnit; +import name.mrkandreev.mapsmith.range.LongBoundType; +import name.mrkandreev.mapsmith.range.LongLongRangeMap; +import name.mrkandreev.mapsmith.range.TreeLongLongRangeMap; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.infra.Blackhole; +import org.openjdk.jmh.runner.Runner; +import org.openjdk.jmh.runner.RunnerException; +import org.openjdk.jmh.runner.options.Options; +import org.openjdk.jmh.runner.options.OptionsBuilder; + +@BenchmarkMode(Mode.Throughput) +@OutputTimeUnit(TimeUnit.SECONDS) +@Warmup(iterations = 5, time = 1) +@Measurement(iterations = 5, time = 1) +@Fork(2) +@Threads(1) +public class LongLongRangeMapBenchmark { + private static final long RANGE_WIDTH = 16L; + private static final long RANGE_STRIDE = 32L; + + public static void main(String[] args) throws RunnerException { + Options options = + new OptionsBuilder().include(LongLongRangeMapBenchmark.class.getSimpleName()).build(); + new Runner(options).run(); + } + + @Benchmark + public void putAll(Ranges ranges, Blackhole blackhole) { + blackhole.consume(putAll(ranges)); + } + + @Benchmark + public void putCoalescingAll(Ranges ranges, Blackhole blackhole) { + blackhole.consume(putCoalescingAll(ranges)); + } + + @Benchmark + public void getExisting(PopulatedMaps maps, Blackhole blackhole) { + blackhole.consume(getExisting(maps.map, maps.existingLookupKeys)); + } + + @Benchmark + public void getMissing(PopulatedMaps maps, Blackhole blackhole) { + blackhole.consume(getMissing(maps.map, maps.missingLookupKeys)); + } + + @Benchmark + public void containsExisting(PopulatedMaps maps, Blackhole blackhole) { + blackhole.consume(containsExisting(maps.map, maps.existingLookupKeys)); + } + + @Benchmark + public void removeAll(RemovableMaps maps, Blackhole blackhole) { + blackhole.consume(removeAll(maps.map, maps.fromInclusive, maps.toInclusive)); + } + + private static LongLongRangeMap putAll(Ranges ranges) { + LongLongRangeMap map = ranges.mapKind.newMap(); + for (int i = 0; i < ranges.fromInclusive.length; i++) { + map.put(ranges.fromInclusive[i], ranges.toInclusive[i], ranges.mappedValues[i]); + } + return map; + } + + private static LongLongRangeMap putCoalescingAll(Ranges ranges) { + LongLongRangeMap map = ranges.mapKind.newMap(); + for (int i = 0; i < ranges.fromInclusive.length; i++) { + map.putCoalescing(ranges.fromInclusive[i], ranges.toInclusive[i], ranges.coalescingValues[i]); + } + return map; + } + + private static long getExisting(LongLongRangeMap map, long[] keys) { + long result = 0L; + for (long key : keys) { + result += map.get(key); + } + return result; + } + + private static long getMissing(LongLongRangeMap map, long[] keys) { + long result = 0L; + for (long key : keys) { + result += map.getOrDefault(key, -1L); + } + return result; + } + + private static int containsExisting(LongLongRangeMap map, long[] keys) { + int result = 0; + for (long key : keys) { + if (map.containsKey(key)) { + result++; + } + } + return result; + } + + private static LongLongRangeMap removeAll(LongLongRangeMap map, long[] from, long[] to) { + for (int i = 0; i < from.length; i++) { + map.remove(from[i], to[i]); + } + return map; + } + + @State(Scope.Thread) + public static class Ranges { + @Param({"RANGE_TREE_MAP", "RANGE_KEY_TREE_MAP"}) + public MapKind mapKind; + + @Param({"1000", "100000"}) + public int size; + + long[] fromInclusive; + long[] toInclusive; + long[] mappedValues; + long[] coalescingValues; + long[] existingLookupKeys; + long[] missingLookupKeys; + + @Setup(Level.Trial) + public void setUp() { + fromInclusive = new long[size]; + toInclusive = new long[size]; + mappedValues = new long[size]; + coalescingValues = new long[size]; + existingLookupKeys = new long[size]; + missingLookupKeys = new long[size]; + + fillRanges( + fromInclusive, + toInclusive, + mappedValues, + coalescingValues, + existingLookupKeys, + missingLookupKeys); + } + } + + public enum MapKind { + RANGE_TREE_MAP { + @Override + LongLongRangeMap newMap() { + return new TreeLongLongRangeMap(); + } + }, + RANGE_KEY_TREE_MAP { + @Override + LongLongRangeMap newMap() { + return new RangeKeyTreeLongLongRangeMap(); + } + }; + + abstract LongLongRangeMap newMap(); + } + + @State(Scope.Thread) + public static class PopulatedMaps extends Ranges { + LongLongRangeMap map; + + @Setup(Level.Trial) + @Override + public void setUp() { + super.setUp(); + + map = putAll(this); + } + } + + @State(Scope.Thread) + public static class RemovableMaps extends Ranges { + LongLongRangeMap map; + + @Setup(Level.Invocation) + @Override + public void setUp() { + super.setUp(); + + map = putAll(this); + } + } + + private static final class RangeKeyTreeLongLongRangeMap implements LongLongRangeMap { + private final NavigableMap ranges = + new TreeMap<>( + Comparator.comparingLong(RangeKey::fromInclusive) + .thenComparingLong(RangeKey::toInclusive)); + + @Override + public int size() { + return ranges.size(); + } + + @Override + public boolean containsKey(long key) { + return rangeFor(key) != null; + } + + @Override + public long get(long key) { + return getOrDefault(key, 0L); + } + + @Override + public long getOrDefault(long key, long defaultValue) { + var range = rangeFor(key); + return range == null ? defaultValue : range.getValue(); + } + + @Override + public void put(long fromInclusive, long toInclusive, long value) { + validateClosedRange(fromInclusive, toInclusive); + putClosed(fromInclusive, toInclusive, value, false); + } + + @Override + public void put( + long lower, LongBoundType lowerType, long upper, LongBoundType upperType, long value) { + Bounds bounds = toClosedBounds(lower, lowerType, upper, upperType); + if (!bounds.isEmpty()) { + putClosed(bounds.fromInclusive(), bounds.toInclusive(), value, false); + } + } + + @Override + public void putCoalescing(long fromInclusive, long toInclusive, long value) { + validateClosedRange(fromInclusive, toInclusive); + putClosed(fromInclusive, toInclusive, value, true); + } + + @Override + public void putCoalescing( + long lower, LongBoundType lowerType, long upper, LongBoundType upperType, long value) { + Bounds bounds = toClosedBounds(lower, lowerType, upper, upperType); + if (!bounds.isEmpty()) { + putClosed(bounds.fromInclusive(), bounds.toInclusive(), value, true); + } + } + + @Override + public void remove(long fromInclusive, long toInclusive) { + validateClosedRange(fromInclusive, toInclusive); + removeClosed(fromInclusive, toInclusive); + } + + @Override + public void remove(long lower, LongBoundType lowerType, long upper, LongBoundType upperType) { + Bounds bounds = toClosedBounds(lower, lowerType, upper, upperType); + if (!bounds.isEmpty()) { + removeClosed(bounds.fromInclusive(), bounds.toInclusive()); + } + } + + @Override + public void clear() { + ranges.clear(); + } + + private void putClosed(long fromInclusive, long toInclusive, long value, boolean coalesce) { + removeClosed(fromInclusive, toInclusive); + RangeKey range = new RangeKey(fromInclusive, toInclusive); + if (coalesce) { + range = mergePrevious(range, value); + range = mergeNext(range, value); + } + ranges.put(range, value); + } + + private void removeClosed(long fromInclusive, long toInclusive) { + var current = ranges.floorEntry(RangeKey.atOrBefore(fromInclusive)); + if (current == null || current.getKey().toInclusive() < fromInclusive) { + current = ranges.ceilingEntry(RangeKey.atOrAfter(fromInclusive)); + } + + while (current != null) { + RangeKey range = current.getKey(); + long value = current.getValue(); + if (range.fromInclusive() > toInclusive) { + return; + } + + var next = ranges.higherEntry(range); + ranges.remove(range); + + if (range.fromInclusive() < fromInclusive) { + ranges.put(new RangeKey(range.fromInclusive(), fromInclusive - 1L), value); + } + if (range.toInclusive() > toInclusive) { + ranges.put(new RangeKey(toInclusive + 1L, range.toInclusive()), value); + return; + } + + current = next; + } + } + + private java.util.Map.Entry rangeFor(long key) { + var entry = ranges.floorEntry(RangeKey.atOrBefore(key)); + if (entry == null || entry.getKey().toInclusive() < key) { + return null; + } + return entry; + } + + private RangeKey mergePrevious(RangeKey range, long value) { + var previous = ranges.lowerEntry(range); + if (previous == null + || previous.getValue() != value + || !touches(previous.getKey().toInclusive(), range.fromInclusive())) { + return range; + } + + ranges.remove(previous.getKey()); + return new RangeKey(previous.getKey().fromInclusive(), range.toInclusive()); + } + + private RangeKey mergeNext(RangeKey range, long value) { + RangeKey result = range; + var next = ranges.ceilingEntry(result); + while (next != null + && next.getValue() == value + && touches(result.toInclusive(), next.getKey().fromInclusive())) { + ranges.remove(next.getKey()); + result = new RangeKey(result.fromInclusive(), next.getKey().toInclusive()); + next = ranges.ceilingEntry(result); + } + return result; + } + } + + private record RangeKey(long fromInclusive, long toInclusive) { + private static RangeKey atOrBefore(long key) { + return new RangeKey(key, Long.MAX_VALUE); + } + + private static RangeKey atOrAfter(long key) { + return new RangeKey(key, Long.MIN_VALUE); + } + } + + private record Bounds(long fromInclusive, long toInclusive) { + private boolean isEmpty() { + return fromInclusive > toInclusive; + } + } + + private static Bounds toClosedBounds( + long lower, LongBoundType lowerType, long upper, LongBoundType upperType) { + if (lowerType == null) { + throw new NullPointerException("lowerType must not be null"); + } + if (upperType == null) { + throw new NullPointerException("upperType must not be null"); + } + if (lower > upper) { + throw new IllegalArgumentException("lower must be <= upper"); + } + if (lower == upper && lowerType == LongBoundType.OPEN && upperType == LongBoundType.OPEN) { + throw new IllegalArgumentException("open range endpoints must be different"); + } + + long fromInclusive = lower; + long toInclusive = upper; + if (lowerType == LongBoundType.OPEN) { + if (lower == Long.MAX_VALUE) { + return new Bounds(1L, 0L); + } + fromInclusive++; + } + if (upperType == LongBoundType.OPEN) { + if (upper == Long.MIN_VALUE) { + return new Bounds(1L, 0L); + } + toInclusive--; + } + return new Bounds(fromInclusive, toInclusive); + } + + private static void validateClosedRange(long fromInclusive, long toInclusive) { + if (fromInclusive > toInclusive) { + throw new IllegalArgumentException("fromInclusive must be <= toInclusive"); + } + } + + private static boolean touches(long leftToInclusive, long rightFromInclusive) { + return leftToInclusive == Long.MAX_VALUE || leftToInclusive + 1L >= rightFromInclusive; + } + + private static void fillRanges( + long[] fromInclusive, + long[] toInclusive, + long[] mappedValues, + long[] coalescingValues, + long[] existingLookupKeys, + long[] missingLookupKeys) { + SplittableRandom random = new SplittableRandom(0x72616e67656d6170L); + for (int i = 0; i < fromInclusive.length; i++) { + long from = i * RANGE_STRIDE; + fromInclusive[i] = from; + toInclusive[i] = from + RANGE_WIDTH - 1L; + mappedValues[i] = random.nextLong(); + coalescingValues[i] = i / 10L; + existingLookupKeys[i] = from + random.nextLong(RANGE_WIDTH); + missingLookupKeys[i] = from + RANGE_WIDTH; + } + } +} diff --git a/mapsmith-benchmarks/src/jmh/java/name/mrkandreev/mapsmith/benchmarks/LongLongRankingMapBenchmark.java b/mapsmith-benchmarks/src/jmh/java/name/mrkandreev/mapsmith/benchmarks/LongLongRankingMapBenchmark.java new file mode 100644 index 0000000..a8d18af --- /dev/null +++ b/mapsmith-benchmarks/src/jmh/java/name/mrkandreev/mapsmith/benchmarks/LongLongRankingMapBenchmark.java @@ -0,0 +1,267 @@ +package name.mrkandreev.mapsmith.benchmarks; + +import java.util.Comparator; +import java.util.SplittableRandom; +import java.util.TreeMap; +import java.util.concurrent.TimeUnit; +import name.mrkandreev.mapsmith.ranking.LongLongRankingMap; +import name.mrkandreev.mapsmith.ranking.OrderStatisticLongLongMap; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.infra.Blackhole; +import org.openjdk.jmh.runner.Runner; +import org.openjdk.jmh.runner.RunnerException; +import org.openjdk.jmh.runner.options.Options; +import org.openjdk.jmh.runner.options.OptionsBuilder; + +@BenchmarkMode(Mode.Throughput) +@OutputTimeUnit(TimeUnit.SECONDS) +@Warmup(iterations = 5, time = 1) +@Measurement(iterations = 5, time = 1) +@Fork(2) +@Threads(1) +public class LongLongRankingMapBenchmark { + public static void main(String[] args) throws RunnerException { + Options options = + new OptionsBuilder().include(LongLongRankingMapBenchmark.class.getSimpleName()).build(); + new Runner(options).run(); + } + + @Benchmark + public void putAll(Keys keys, Blackhole blackhole) { + blackhole.consume(putAll(keys)); + } + + @Benchmark + public void putExisting(PopulatedMaps maps, Blackhole blackhole) { + blackhole.consume(putExisting(maps.map, maps.lookupKeys, maps.updatedValues)); + } + + @Benchmark + public void rankOfExisting(PopulatedMaps maps, Blackhole blackhole) { + blackhole.consume(rankOfExisting(maps.map, maps.lookupKeys)); + } + + @Benchmark + public void countBeforeExisting(PopulatedMaps maps, Blackhole blackhole) { + blackhole.consume(countBeforeExisting(maps.map, maps.lookupKeys)); + } + + @Benchmark + public void countAfterExisting(PopulatedMaps maps, Blackhole blackhole) { + blackhole.consume(countAfterExisting(maps.map, maps.lookupKeys)); + } + + @Benchmark + public void removeAll(PopulatedMaps maps, Blackhole blackhole) { + blackhole.consume(removeAll(maps.map, maps.lookupKeys)); + } + + private static LongLongRankingMap putAll(Keys keys) { + LongLongRankingMap map = keys.mapKind.newMap(keys.size); + for (int i = 0; i < keys.lookupKeys.length; i++) { + map.put(keys.lookupKeys[i], keys.mappedValues[i]); + } + return map; + } + + private static long putExisting(LongLongRankingMap map, long[] keys, long[] values) { + long result = 0L; + for (int i = 0; i < keys.length; i++) { + result += map.put(keys[i], values[i]); + } + return result; + } + + private static long rankOfExisting(LongLongRankingMap map, long[] keys) { + long result = 0L; + for (long key : keys) { + result += map.rankOf(key); + } + return result; + } + + private static long countBeforeExisting(LongLongRankingMap map, long[] keys) { + long result = 0L; + for (long key : keys) { + result += map.countBefore(key); + } + return result; + } + + private static long countAfterExisting(LongLongRankingMap map, long[] keys) { + long result = 0L; + for (long key : keys) { + result += map.countAfter(key); + } + return result; + } + + private static long removeAll(LongLongRankingMap map, long[] keys) { + long result = 0L; + for (long key : keys) { + result += map.remove(key); + } + return result; + } + + @State(Scope.Thread) + public static class Keys { + @Param({"ORDER_STATISTIC", "TREE_MAP"}) + public MapKind mapKind; + + @Param({"1000", "100000"}) + public int size; + + long[] lookupKeys; + long[] mappedValues; + long[] updatedValues; + + @Setup(Level.Trial) + public void setUp() { + lookupKeys = new long[size]; + mappedValues = new long[size]; + updatedValues = new long[size]; + + fillKeys(lookupKeys, mappedValues, updatedValues); + } + } + + public enum MapKind { + ORDER_STATISTIC { + @Override + LongLongRankingMap newMap(int expectedSize) { + return new OrderStatisticLongLongMap(expectedSize); + } + }, + TREE_MAP { + @Override + LongLongRankingMap newMap(int expectedSize) { + return new TreeLongLongRankingMap(); + } + }; + + abstract LongLongRankingMap newMap(int expectedSize); + } + + @State(Scope.Thread) + public static class PopulatedMaps extends Keys { + LongLongRankingMap map; + + @Setup(Level.Invocation) + @Override + public void setUp() { + super.setUp(); + + map = mapKind.newMap(size); + for (int i = 0; i < lookupKeys.length; i++) { + map.put(lookupKeys[i], mappedValues[i]); + } + } + } + + private static final class TreeLongLongRankingMap implements LongLongRankingMap { + private final TreeMap valueByKey = new TreeMap<>(); + private final TreeMap keyByRank = + new TreeMap<>( + Comparator.comparingLong(RankKey::value).reversed().thenComparingLong(RankKey::key)); + + @Override + public int size() { + return valueByKey.size(); + } + + @Override + public boolean containsKey(long key) { + return valueByKey.containsKey(key); + } + + @Override + public long get(long key) { + return getOrDefault(key, 0L); + } + + @Override + public long getOrDefault(long key, long defaultValue) { + return valueByKey.getOrDefault(key, defaultValue); + } + + @Override + public long put(long key, long value) { + Long previousValue = valueByKey.put(key, value); + if (previousValue != null) { + keyByRank.remove(new RankKey(key, previousValue)); + } + keyByRank.put(new RankKey(key, value), key); + return previousValue == null ? 0L : previousValue; + } + + @Override + public long remove(long key) { + Long previousValue = valueByKey.remove(key); + if (previousValue == null) { + return 0L; + } + keyByRank.remove(new RankKey(key, previousValue)); + return previousValue; + } + + @Override + public void clear() { + valueByKey.clear(); + keyByRank.clear(); + } + + @Override + public int rankOf(long key) { + int countBefore = countBefore(key); + return countBefore == MISSING_RANK ? MISSING_RANK : countBefore + 1; + } + + @Override + public int countBefore(long key) { + Long value = valueByKey.get(key); + if (value == null) { + return MISSING_RANK; + } + + int count = 0; + RankKey requested = new RankKey(key, value); + for (RankKey current : keyByRank.keySet()) { + if (current.equals(requested)) { + return count; + } + count++; + } + return MISSING_RANK; + } + + @Override + public int countAfter(long key) { + int countBefore = countBefore(key); + return countBefore == MISSING_RANK ? MISSING_RANK : size() - countBefore - 1; + } + } + + private record RankKey(long key, long value) {} + + private static void fillKeys(long[] lookupKeys, long[] mappedValues, long[] updatedValues) { + SplittableRandom random = new SplittableRandom(0x72616e6b696e6773L); + for (int i = 0; i < lookupKeys.length; i++) { + lookupKeys[i] = random.nextLong(); + mappedValues[i] = random.nextLong(); + updatedValues[i] = random.nextLong(); + } + } +} diff --git a/mapsmith-benchmarks/src/jmh/java/name/mrkandreev/mapsmith/benchmarks/LongObjectMapBenchmark.java b/mapsmith-benchmarks/src/jmh/java/name/mrkandreev/mapsmith/benchmarks/LongObjectMapBenchmark.java new file mode 100644 index 0000000..125597a --- /dev/null +++ b/mapsmith-benchmarks/src/jmh/java/name/mrkandreev/mapsmith/benchmarks/LongObjectMapBenchmark.java @@ -0,0 +1,192 @@ +package name.mrkandreev.mapsmith.benchmarks; + +import java.util.HashMap; +import java.util.Map; +import java.util.SplittableRandom; +import java.util.concurrent.TimeUnit; +import name.mrkandreev.mapsmith.LongObjectMap; +import name.mrkandreev.mapsmith.openaddressing.LongHashing; +import name.mrkandreev.mapsmith.openaddressing.LongObjectMapFactory; +import name.mrkandreev.mapsmith.openaddressing.MapSpecialization; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.infra.Blackhole; + +@BenchmarkMode(Mode.Throughput) +@OutputTimeUnit(TimeUnit.SECONDS) +@Warmup(iterations = 5, time = 1) +@Measurement(iterations = 5, time = 1) +@Fork(2) +public class LongObjectMapBenchmark { + @Benchmark + public void getExisting(PopulatedMaps maps, Blackhole blackhole) { + blackhole.consume(getExisting(maps.map, maps.lookupKeys)); + } + + @Benchmark + public void putAll(Keys keys, Blackhole blackhole) { + blackhole.consume(putAll(keys)); + } + + private static int getExisting(LongObjectMap map, long[] lookupKeys) { + int result = 0; + for (long key : lookupKeys) { + result += map.get(key).length(); + } + return result; + } + + private static LongObjectMap putAll(Keys keys) { + LongObjectMap map = keys.mapKind.newMap(keys.size); + for (int i = 0; i < keys.lookupKeys.length; i++) { + map.put(keys.lookupKeys[i], keys.mappedValues[i]); + } + return map; + } + + @State(Scope.Thread) + public static class Keys { + @Param({ + "LINEAR_PROBING_MURMUR3_FINALIZER", + "LINEAR_PROBING_FIBONACCI", + "LINEAR_PROBING_XOR_SHIFT", + "LINEAR_PROBING_IDENTITY", + "ROBIN_HOOD_MURMUR3_FINALIZER", + "ROBIN_HOOD_FIBONACCI", + "ROBIN_HOOD_XOR_SHIFT", + "ROBIN_HOOD_IDENTITY", + "SWISS_TABLE_MURMUR3_FINALIZER", + "SWISS_TABLE_FIBONACCI", + "SWISS_TABLE_XOR_SHIFT", + "SWISS_TABLE_IDENTITY", + "HASH_MAP" + }) + public MapKind mapKind; + + @Param({"1000", "100000"}) + public int size; + + long[] lookupKeys; + String[] mappedValues; + + @Setup(Level.Trial) + public void setUp() { + lookupKeys = new long[size]; + mappedValues = new String[size]; + + fillKeys(lookupKeys, mappedValues); + } + } + + public enum MapKind { + LINEAR_PROBING_MURMUR3_FINALIZER( + MapSpecialization.LINEAR_PROBING, LongHashing.MURMUR3_FINALIZER), + LINEAR_PROBING_FIBONACCI(MapSpecialization.LINEAR_PROBING, LongHashing.FIBONACCI), + LINEAR_PROBING_XOR_SHIFT(MapSpecialization.LINEAR_PROBING, LongHashing.XOR_SHIFT), + LINEAR_PROBING_IDENTITY(MapSpecialization.LINEAR_PROBING, LongHashing.IDENTITY), + ROBIN_HOOD_MURMUR3_FINALIZER(MapSpecialization.ROBIN_HOOD, LongHashing.MURMUR3_FINALIZER), + ROBIN_HOOD_FIBONACCI(MapSpecialization.ROBIN_HOOD, LongHashing.FIBONACCI), + ROBIN_HOOD_XOR_SHIFT(MapSpecialization.ROBIN_HOOD, LongHashing.XOR_SHIFT), + ROBIN_HOOD_IDENTITY(MapSpecialization.ROBIN_HOOD, LongHashing.IDENTITY), + SWISS_TABLE_MURMUR3_FINALIZER(MapSpecialization.SWISS_TABLE, LongHashing.MURMUR3_FINALIZER), + SWISS_TABLE_FIBONACCI(MapSpecialization.SWISS_TABLE, LongHashing.FIBONACCI), + SWISS_TABLE_XOR_SHIFT(MapSpecialization.SWISS_TABLE, LongHashing.XOR_SHIFT), + SWISS_TABLE_IDENTITY(MapSpecialization.SWISS_TABLE, LongHashing.IDENTITY), + HASH_MAP(null, null); + + private final MapSpecialization mapSpecialization; + private final LongHashing hashing; + + MapKind(MapSpecialization specialization, LongHashing hashing) { + mapSpecialization = specialization; + this.hashing = hashing; + } + + private LongObjectMap newMap(int expectedSize) { + if (this == HASH_MAP) { + return new HashMapLongObjectMap<>(expectedSize); + } + return LongObjectMapFactory.create(mapSpecialization, expectedSize, hashing); + } + } + + @State(Scope.Thread) + public static class PopulatedMaps extends Keys { + LongObjectMap map; + + @Setup(Level.Trial) + @Override + public void setUp() { + super.setUp(); + + map = mapKind.newMap(size); + + for (int i = 0; i < lookupKeys.length; i++) { + map.put(lookupKeys[i], mappedValues[i]); + } + } + } + + private record HashMapLongObjectMap(Map delegate) implements LongObjectMap { + private HashMapLongObjectMap(int expectedSize) { + this(new HashMap<>(hashMapCapacity(expectedSize))); + } + + @Override + public int size() { + return delegate.size(); + } + + @Override + public boolean containsKey(long key) { + return delegate.containsKey(key); + } + + @Override + public T get(long key) { + return delegate.get(key); + } + + @Override + public T getOrDefault(long key, T defaultValue) { + return delegate.getOrDefault(key, defaultValue); + } + + @Override + public T put(long key, T value) { + return delegate.put(key, value); + } + + @Override + public T remove(long key) { + return delegate.remove(key); + } + + @Override + public void clear() { + delegate.clear(); + } + + private static int hashMapCapacity(int size) { + return (int) (size / 0.75f) + 1; + } + } + + private static void fillKeys(long[] lookupKeys, String[] mappedValues) { + SplittableRandom random = new SplittableRandom(0x4d6170736d697468L); + for (int i = 0; i < lookupKeys.length; i++) { + lookupKeys[i] = random.nextLong(); + mappedValues[i] = "value-" + i; + } + } +} diff --git a/mapsmith-benchmarks/src/jmh/java/name/mrkandreev/mapsmith/benchmarks/LongObjectRangeMapBenchmark.java b/mapsmith-benchmarks/src/jmh/java/name/mrkandreev/mapsmith/benchmarks/LongObjectRangeMapBenchmark.java new file mode 100644 index 0000000..6b2f9ce --- /dev/null +++ b/mapsmith-benchmarks/src/jmh/java/name/mrkandreev/mapsmith/benchmarks/LongObjectRangeMapBenchmark.java @@ -0,0 +1,187 @@ +package name.mrkandreev.mapsmith.benchmarks; + +import java.util.SplittableRandom; +import java.util.concurrent.TimeUnit; +import name.mrkandreev.mapsmith.range.LongObjectRangeMap; +import name.mrkandreev.mapsmith.range.TreeLongObjectRangeMap; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.infra.Blackhole; + +@BenchmarkMode(Mode.Throughput) +@OutputTimeUnit(TimeUnit.SECONDS) +@Warmup(iterations = 5, time = 1) +@Measurement(iterations = 5, time = 1) +@Fork(2) +@Threads(1) +public class LongObjectRangeMapBenchmark { + private static final long RANGE_WIDTH = 16L; + private static final long RANGE_STRIDE = 32L; + + @Benchmark + public void putAll(Ranges ranges, Blackhole blackhole) { + blackhole.consume(putAll(ranges)); + } + + @Benchmark + public void putCoalescingAll(Ranges ranges, Blackhole blackhole) { + blackhole.consume(putCoalescingAll(ranges)); + } + + @Benchmark + public void getExisting(PopulatedMaps maps, Blackhole blackhole) { + blackhole.consume(getExisting(maps.map, maps.existingLookupKeys)); + } + + @Benchmark + public void getMissing(PopulatedMaps maps, Blackhole blackhole) { + blackhole.consume(getMissing(maps.map, maps.missingLookupKeys)); + } + + @Benchmark + public void containsExisting(PopulatedMaps maps, Blackhole blackhole) { + blackhole.consume(containsExisting(maps.map, maps.existingLookupKeys)); + } + + @Benchmark + public void removeAll(RemovableMaps maps, Blackhole blackhole) { + blackhole.consume(removeAll(maps.map, maps.fromInclusive, maps.toInclusive)); + } + + private static LongObjectRangeMap putAll(Ranges ranges) { + LongObjectRangeMap map = new TreeLongObjectRangeMap<>(); + for (int i = 0; i < ranges.fromInclusive.length; i++) { + map.put(ranges.fromInclusive[i], ranges.toInclusive[i], ranges.mappedValues[i]); + } + return map; + } + + private static LongObjectRangeMap putCoalescingAll(Ranges ranges) { + LongObjectRangeMap map = new TreeLongObjectRangeMap<>(); + for (int i = 0; i < ranges.fromInclusive.length; i++) { + map.putCoalescing(ranges.fromInclusive[i], ranges.toInclusive[i], ranges.coalescingValues[i]); + } + return map; + } + + private static int getExisting(LongObjectRangeMap map, long[] keys) { + int result = 0; + for (long key : keys) { + result += map.get(key).length(); + } + return result; + } + + private static int getMissing(LongObjectRangeMap map, long[] keys) { + int result = 0; + for (long key : keys) { + result += map.getOrDefault(key, "missing").length(); + } + return result; + } + + private static int containsExisting(LongObjectRangeMap map, long[] keys) { + int result = 0; + for (long key : keys) { + if (map.containsKey(key)) { + result++; + } + } + return result; + } + + private static LongObjectRangeMap removeAll( + LongObjectRangeMap map, long[] from, long[] to) { + for (int i = 0; i < from.length; i++) { + map.remove(from[i], to[i]); + } + return map; + } + + @State(Scope.Thread) + public static class Ranges { + @Param({"1000", "100000"}) + public int size; + + long[] fromInclusive; + long[] toInclusive; + String[] mappedValues; + String[] coalescingValues; + long[] existingLookupKeys; + long[] missingLookupKeys; + + @Setup(Level.Trial) + public void setUp() { + fromInclusive = new long[size]; + toInclusive = new long[size]; + mappedValues = new String[size]; + coalescingValues = new String[size]; + existingLookupKeys = new long[size]; + missingLookupKeys = new long[size]; + + fillRanges( + fromInclusive, + toInclusive, + mappedValues, + coalescingValues, + existingLookupKeys, + missingLookupKeys); + } + } + + @State(Scope.Thread) + public static class PopulatedMaps extends Ranges { + LongObjectRangeMap map; + + @Setup(Level.Trial) + @Override + public void setUp() { + super.setUp(); + + map = putAll(this); + } + } + + @State(Scope.Thread) + public static class RemovableMaps extends Ranges { + LongObjectRangeMap map; + + @Setup(Level.Invocation) + @Override + public void setUp() { + super.setUp(); + + map = putAll(this); + } + } + + private static void fillRanges( + long[] fromInclusive, + long[] toInclusive, + String[] mappedValues, + String[] coalescingValues, + long[] existingLookupKeys, + long[] missingLookupKeys) { + SplittableRandom random = new SplittableRandom(0x72616e67656d6170L); + for (int i = 0; i < fromInclusive.length; i++) { + long from = i * RANGE_STRIDE; + fromInclusive[i] = from; + toInclusive[i] = from + RANGE_WIDTH - 1L; + mappedValues[i] = "value-" + random.nextLong(); + coalescingValues[i] = "bucket-" + i / 10; + existingLookupKeys[i] = from + random.nextLong(RANGE_WIDTH); + missingLookupKeys[i] = from + RANGE_WIDTH; + } + } +} diff --git a/mapsmith-core/build.gradle.kts b/mapsmith-core/build.gradle.kts new file mode 100644 index 0000000..0ca308f --- /dev/null +++ b/mapsmith-core/build.gradle.kts @@ -0,0 +1,40 @@ +plugins { + `java-library` + alias(libs.plugins.maven.publish) +} + +mavenPublishing { + coordinates(project.group.toString(), "mapsmith-core", project.version.toString()) + + publishToMavenCentral(automaticRelease = true) + signAllPublications() + + pom { + name = "mapsmith-core" + description = "High-performance primitive map implementations for Java." + inceptionYear = "2026" + url = "https://github.com/mrk-andreev/mapsmith" + + licenses { + license { + name = "MIT License" + url = "https://opensource.org/license/mit/" + distribution = "repo" + } + } + + developers { + developer { + id = "mrk-andreev" + name = "Mark Andreev" + url = "https://github.com/mrk-andreev" + } + } + + scm { + url = "https://github.com/mrk-andreev/mapsmith" + connection = "scm:git:https://github.com/mrk-andreev/mapsmith.git" + developerConnection = "scm:git:ssh://git@github.com:mrk-andreev/mapsmith.git" + } + } +} diff --git a/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/LongLongMap.java b/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/LongLongMap.java new file mode 100644 index 0000000..71b63fc --- /dev/null +++ b/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/LongLongMap.java @@ -0,0 +1,21 @@ +package name.mrkandreev.mapsmith; + +public interface LongLongMap { + int size(); + + default boolean isEmpty() { + return size() == 0; + } + + boolean containsKey(long key); + + long get(long key); + + long getOrDefault(long key, long defaultValue); + + long put(long key, long value); + + long remove(long key); + + void clear(); +} diff --git a/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/LongObjectMap.java b/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/LongObjectMap.java new file mode 100644 index 0000000..bd2a98a --- /dev/null +++ b/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/LongObjectMap.java @@ -0,0 +1,21 @@ +package name.mrkandreev.mapsmith; + +public interface LongObjectMap { + int size(); + + default boolean isEmpty() { + return size() == 0; + } + + boolean containsKey(long key); + + T get(long key); + + T getOrDefault(long key, T defaultValue); + + T put(long key, T value); + + T remove(long key); + + void clear(); +} diff --git a/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/LongHashing.java b/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/LongHashing.java new file mode 100644 index 0000000..bebdda4 --- /dev/null +++ b/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/LongHashing.java @@ -0,0 +1,40 @@ +package name.mrkandreev.mapsmith.openaddressing; + +public enum LongHashing { + MURMUR3_FINALIZER { + @Override + public long hash(long value) { + long result = value; + result ^= result >>> 33; + result *= 0xff51afd7ed558ccdL; + result ^= result >>> 33; + result *= 0xc4ceb9fe1a85ec53L; + result ^= result >>> 33; + return result; + } + }, + FIBONACCI { + @Override + public long hash(long value) { + return value * 0x9e3779b97f4a7c15L; + } + }, + XOR_SHIFT { + @Override + public long hash(long value) { + long result = value; + result ^= result >>> 32; + result *= 0xd6e8feb86659fd93L; + result ^= result >>> 32; + return result; + } + }, + IDENTITY { + @Override + public long hash(long value) { + return value; + } + }; + + public abstract long hash(long value); +} diff --git a/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/LongLongMapFactory.java b/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/LongLongMapFactory.java new file mode 100644 index 0000000..a27b5b5 --- /dev/null +++ b/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/LongLongMapFactory.java @@ -0,0 +1,31 @@ +package name.mrkandreev.mapsmith.openaddressing; + +import java.util.Objects; +import name.mrkandreev.mapsmith.LongLongMap; + +public final class LongLongMapFactory { + private LongLongMapFactory() {} + + public static LongLongMap create(MapSpecialization specialization) { + return create(specialization, LongHashing.MURMUR3_FINALIZER); + } + + public static LongLongMap create(MapSpecialization specialization, int expectedSize) { + return create(specialization, expectedSize, LongHashing.MURMUR3_FINALIZER); + } + + public static LongLongMap create(MapSpecialization specialization, LongHashing hashing) { + Objects.requireNonNull(specialization, "specialization must not be null"); + Objects.requireNonNull(hashing, "hashing must not be null"); + + return new LongLongOpenAddressMap(specialization.strategy(), hashing); + } + + public static LongLongMap create( + MapSpecialization specialization, int expectedSize, LongHashing hashing) { + Objects.requireNonNull(specialization, "specialization must not be null"); + Objects.requireNonNull(hashing, "hashing must not be null"); + + return new LongLongOpenAddressMap(specialization.strategy(), expectedSize, hashing); + } +} diff --git a/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/LongLongOpenAddressMap.java b/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/LongLongOpenAddressMap.java new file mode 100644 index 0000000..0655a86 --- /dev/null +++ b/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/LongLongOpenAddressMap.java @@ -0,0 +1,74 @@ +package name.mrkandreev.mapsmith.openaddressing; + +import java.util.Objects; +import name.mrkandreev.mapsmith.LongLongMap; + +public final class LongLongOpenAddressMap implements LongLongMap { + public static final int DEFAULT_EXPECTED_SIZE = 16; + + private final LongLongMap delegate; + + public LongLongOpenAddressMap() { + this(LongLongOpenAddressingStrategy.LINEAR_PROBING); + } + + public LongLongOpenAddressMap(int expectedSize) { + this(LongLongOpenAddressingStrategy.LINEAR_PROBING, expectedSize); + } + + public LongLongOpenAddressMap(LongLongOpenAddressingStrategy strategy) { + this(strategy, DEFAULT_EXPECTED_SIZE); + } + + public LongLongOpenAddressMap(LongLongOpenAddressingStrategy strategy, int expectedSize) { + this(strategy, expectedSize, LongHashing.MURMUR3_FINALIZER); + } + + public LongLongOpenAddressMap(LongLongOpenAddressingStrategy strategy, LongHashing hashing) { + this(strategy, DEFAULT_EXPECTED_SIZE, hashing); + } + + public LongLongOpenAddressMap( + LongLongOpenAddressingStrategy strategy, int expectedSize, LongHashing hashing) { + Objects.requireNonNull(strategy, "strategy must not be null"); + Objects.requireNonNull(hashing, "hashing must not be null"); + delegate = + Objects.requireNonNull( + strategy.create(expectedSize, hashing), "strategy must not create a null map"); + } + + @Override + public int size() { + return delegate.size(); + } + + @Override + public boolean containsKey(long key) { + return delegate.containsKey(key); + } + + @Override + public long get(long key) { + return delegate.get(key); + } + + @Override + public long getOrDefault(long key, long defaultValue) { + return delegate.getOrDefault(key, defaultValue); + } + + @Override + public long put(long key, long value) { + return delegate.put(key, value); + } + + @Override + public long remove(long key) { + return delegate.remove(key); + } + + @Override + public void clear() { + delegate.clear(); + } +} diff --git a/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/LongLongOpenAddressingStrategy.java b/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/LongLongOpenAddressingStrategy.java new file mode 100644 index 0000000..5b0337a --- /dev/null +++ b/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/LongLongOpenAddressingStrategy.java @@ -0,0 +1,15 @@ +package name.mrkandreev.mapsmith.openaddressing; + +import name.mrkandreev.mapsmith.LongLongMap; +import name.mrkandreev.mapsmith.openaddressing.strategies.LinearProbingLongLongMap; +import name.mrkandreev.mapsmith.openaddressing.strategies.RobinHoodLongLongMap; +import name.mrkandreev.mapsmith.openaddressing.strategies.SwissTableLongLongMap; + +@FunctionalInterface +public interface LongLongOpenAddressingStrategy { + LongLongOpenAddressingStrategy LINEAR_PROBING = LinearProbingLongLongMap::new; + LongLongOpenAddressingStrategy ROBIN_HOOD = RobinHoodLongLongMap::new; + LongLongOpenAddressingStrategy SWISS_TABLE = SwissTableLongLongMap::new; + + LongLongMap create(int expectedSize, LongHashing hashing); +} diff --git a/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/LongObjectMapFactory.java b/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/LongObjectMapFactory.java new file mode 100644 index 0000000..99150c7 --- /dev/null +++ b/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/LongObjectMapFactory.java @@ -0,0 +1,31 @@ +package name.mrkandreev.mapsmith.openaddressing; + +import java.util.Objects; +import name.mrkandreev.mapsmith.LongObjectMap; + +public final class LongObjectMapFactory { + private LongObjectMapFactory() {} + + public static LongObjectMap create(MapSpecialization specialization) { + return create(specialization, LongHashing.MURMUR3_FINALIZER); + } + + public static LongObjectMap create(MapSpecialization specialization, int expectedSize) { + return create(specialization, expectedSize, LongHashing.MURMUR3_FINALIZER); + } + + public static LongObjectMap create(MapSpecialization specialization, LongHashing hashing) { + Objects.requireNonNull(specialization, "specialization must not be null"); + Objects.requireNonNull(hashing, "hashing must not be null"); + + return new LongObjectOpenAddressMap<>(specialization.objectStrategy(), hashing); + } + + public static LongObjectMap create( + MapSpecialization specialization, int expectedSize, LongHashing hashing) { + Objects.requireNonNull(specialization, "specialization must not be null"); + Objects.requireNonNull(hashing, "hashing must not be null"); + + return new LongObjectOpenAddressMap<>(specialization.objectStrategy(), expectedSize, hashing); + } +} diff --git a/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/LongObjectOpenAddressMap.java b/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/LongObjectOpenAddressMap.java new file mode 100644 index 0000000..b008e02 --- /dev/null +++ b/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/LongObjectOpenAddressMap.java @@ -0,0 +1,80 @@ +package name.mrkandreev.mapsmith.openaddressing; + +import java.util.Objects; +import name.mrkandreev.mapsmith.LongObjectMap; + +public final class LongObjectOpenAddressMap implements LongObjectMap { + public static final int DEFAULT_EXPECTED_SIZE = 16; + + private final LongObjectMap delegate; + + public LongObjectOpenAddressMap() { + this(defaultStrategy()); + } + + public LongObjectOpenAddressMap(int expectedSize) { + this(defaultStrategy(), expectedSize); + } + + public LongObjectOpenAddressMap(LongObjectOpenAddressingStrategy strategy) { + this(strategy, DEFAULT_EXPECTED_SIZE); + } + + public LongObjectOpenAddressMap(LongObjectOpenAddressingStrategy strategy, int expectedSize) { + this(strategy, expectedSize, LongHashing.MURMUR3_FINALIZER); + } + + public LongObjectOpenAddressMap( + LongObjectOpenAddressingStrategy strategy, LongHashing hashing) { + this(strategy, DEFAULT_EXPECTED_SIZE, hashing); + } + + public LongObjectOpenAddressMap( + LongObjectOpenAddressingStrategy strategy, int expectedSize, LongHashing hashing) { + Objects.requireNonNull(strategy, "strategy must not be null"); + Objects.requireNonNull(hashing, "hashing must not be null"); + delegate = + Objects.requireNonNull( + strategy.create(expectedSize, hashing), "strategy must not create a null map"); + } + + @Override + public int size() { + return delegate.size(); + } + + @Override + public boolean containsKey(long key) { + return delegate.containsKey(key); + } + + @Override + public T get(long key) { + return delegate.get(key); + } + + @Override + public T getOrDefault(long key, T defaultValue) { + return delegate.getOrDefault(key, defaultValue); + } + + @Override + public T put(long key, T value) { + return delegate.put(key, value); + } + + @Override + public T remove(long key) { + return delegate.remove(key); + } + + @Override + public void clear() { + delegate.clear(); + } + + @SuppressWarnings("unchecked") + private static LongObjectOpenAddressingStrategy defaultStrategy() { + return LongObjectOpenAddressingStrategy.LINEAR_PROBING; + } +} diff --git a/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/LongObjectOpenAddressingStrategy.java b/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/LongObjectOpenAddressingStrategy.java new file mode 100644 index 0000000..1bc985f --- /dev/null +++ b/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/LongObjectOpenAddressingStrategy.java @@ -0,0 +1,20 @@ +package name.mrkandreev.mapsmith.openaddressing; + +import name.mrkandreev.mapsmith.LongObjectMap; +import name.mrkandreev.mapsmith.openaddressing.strategies.LinearProbingLongObjectMap; +import name.mrkandreev.mapsmith.openaddressing.strategies.RobinHoodLongObjectMap; +import name.mrkandreev.mapsmith.openaddressing.strategies.SwissTableLongObjectMap; + +@FunctionalInterface +public interface LongObjectOpenAddressingStrategy { + @SuppressWarnings("rawtypes") + LongObjectOpenAddressingStrategy LINEAR_PROBING = LinearProbingLongObjectMap::new; + + @SuppressWarnings("rawtypes") + LongObjectOpenAddressingStrategy ROBIN_HOOD = RobinHoodLongObjectMap::new; + + @SuppressWarnings("rawtypes") + LongObjectOpenAddressingStrategy SWISS_TABLE = SwissTableLongObjectMap::new; + + LongObjectMap create(int expectedSize, LongHashing hashing); +} diff --git a/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/MapSpecialization.java b/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/MapSpecialization.java new file mode 100644 index 0000000..5edd385 --- /dev/null +++ b/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/MapSpecialization.java @@ -0,0 +1,47 @@ +package name.mrkandreev.mapsmith.openaddressing; + +public enum MapSpecialization { + LINEAR_PROBING { + @Override + public LongLongOpenAddressingStrategy strategy() { + return LongLongOpenAddressingStrategy.LINEAR_PROBING; + } + + @Override + public LongObjectOpenAddressingStrategy objectStrategy() { + return castObjectStrategy(LongObjectOpenAddressingStrategy.LINEAR_PROBING); + } + }, + ROBIN_HOOD { + @Override + public LongLongOpenAddressingStrategy strategy() { + return LongLongOpenAddressingStrategy.ROBIN_HOOD; + } + + @Override + public LongObjectOpenAddressingStrategy objectStrategy() { + return castObjectStrategy(LongObjectOpenAddressingStrategy.ROBIN_HOOD); + } + }, + SWISS_TABLE { + @Override + public LongLongOpenAddressingStrategy strategy() { + return LongLongOpenAddressingStrategy.SWISS_TABLE; + } + + @Override + public LongObjectOpenAddressingStrategy objectStrategy() { + return castObjectStrategy(LongObjectOpenAddressingStrategy.SWISS_TABLE); + } + }; + + public abstract LongLongOpenAddressingStrategy strategy(); + + public abstract LongObjectOpenAddressingStrategy objectStrategy(); + + @SuppressWarnings("unchecked") + private static LongObjectOpenAddressingStrategy castObjectStrategy( + LongObjectOpenAddressingStrategy strategy) { + return (LongObjectOpenAddressingStrategy) strategy; + } +} diff --git a/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/strategies/LinearProbingLongLongMap.java b/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/strategies/LinearProbingLongLongMap.java new file mode 100644 index 0000000..66b5300 --- /dev/null +++ b/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/strategies/LinearProbingLongLongMap.java @@ -0,0 +1,183 @@ +package name.mrkandreev.mapsmith.openaddressing.strategies; + +import java.util.Arrays; +import java.util.Objects; +import name.mrkandreev.mapsmith.LongLongMap; +import name.mrkandreev.mapsmith.openaddressing.LongHashing; + +public final class LinearProbingLongLongMap implements LongLongMap { + private static final byte EMPTY = 0; + private static final byte OCCUPIED = 1; + private static final byte DELETED = 2; + + private long[] keys; + private long[] values; + private byte[] states; + private int mask; + private int resizeThreshold; + private int entryCount; + private int used; + private final LongHashing hashing; + + public LinearProbingLongLongMap() { + this(LongLongHashSupport.DEFAULT_EXPECTED_SIZE); + } + + public LinearProbingLongLongMap(int expectedSize) { + this(expectedSize, LongHashing.MURMUR3_FINALIZER); + } + + public LinearProbingLongLongMap(LongHashing hashing) { + this(LongLongHashSupport.DEFAULT_EXPECTED_SIZE, hashing); + } + + public LinearProbingLongLongMap(int expectedSize, LongHashing hashing) { + this.hashing = Objects.requireNonNull(hashing, "hashing must not be null"); + allocate(LongLongHashSupport.capacityFor(expectedSize)); + } + + @Override + public int size() { + return entryCount; + } + + @Override + public boolean containsKey(long key) { + return findIndex(key) >= 0; + } + + @Override + public long get(long key) { + return getOrDefault(key, 0L); + } + + @Override + public long getOrDefault(long key, long defaultValue) { + int index = findIndex(key); + return index >= 0 ? values[index] : defaultValue; + } + + @Override + public long put(long key, long value) { + ensureInsertCapacity(); + + int index = insertionIndex(key); + if (index >= 0) { + long previousValue = values[index]; + values[index] = value; + return previousValue; + } + + int insertionIndex = -index - 1; + if (states[insertionIndex] == EMPTY) { + used++; + } + states[insertionIndex] = OCCUPIED; + keys[insertionIndex] = key; + values[insertionIndex] = value; + entryCount++; + return 0L; + } + + @Override + public long remove(long key) { + int index = findIndex(key); + if (index < 0) { + return 0L; + } + + long previousValue = values[index]; + states[index] = DELETED; + entryCount--; + return previousValue; + } + + @Override + public void clear() { + Arrays.fill(states, EMPTY); + entryCount = 0; + used = 0; + } + + private int findIndex(long key) { + int index = (int) hashing.hash(key) & mask; + while (true) { + byte state = states[index]; + if (state == EMPTY) { + return -1; + } + if (state == OCCUPIED && keys[index] == key) { + return index; + } + index = (index + 1) & mask; + } + } + + private int insertionIndex(long key) { + int index = (int) hashing.hash(key) & mask; + int firstDeletedIndex = -1; + + while (true) { + byte state = states[index]; + if (state == EMPTY) { + return firstDeletedIndex >= 0 ? -firstDeletedIndex - 1 : -index - 1; + } + if (state == OCCUPIED && keys[index] == key) { + return index; + } + if (state == DELETED && firstDeletedIndex < 0) { + firstDeletedIndex = index; + } + index = (index + 1) & mask; + } + } + + private void ensureInsertCapacity() { + if (used + 1 <= resizeThreshold) { + return; + } + + int nextCapacity = + entryCount + 1 <= resizeThreshold + ? keys.length + : LongLongHashSupport.nextCapacity(keys.length); + rehash(nextCapacity); + } + + private void rehash(int capacity) { + long[] oldKeys = keys; + long[] oldValues = values; + byte[] oldStates = states; + + allocate(capacity); + + for (int i = 0; i < oldStates.length; i++) { + if (oldStates[i] == OCCUPIED) { + insertRehashed(oldKeys[i], oldValues[i]); + } + } + } + + private void insertRehashed(long key, long value) { + int index = (int) hashing.hash(key) & mask; + while (states[index] == OCCUPIED) { + index = (index + 1) & mask; + } + + states[index] = OCCUPIED; + keys[index] = key; + values[index] = value; + entryCount++; + used++; + } + + private void allocate(int capacity) { + keys = new long[capacity]; + values = new long[capacity]; + states = new byte[capacity]; + mask = capacity - 1; + resizeThreshold = LongLongHashSupport.computeResizeThreshold(capacity); + entryCount = 0; + used = 0; + } +} diff --git a/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/strategies/LinearProbingLongObjectMap.java b/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/strategies/LinearProbingLongObjectMap.java new file mode 100644 index 0000000..9d58652 --- /dev/null +++ b/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/strategies/LinearProbingLongObjectMap.java @@ -0,0 +1,199 @@ +package name.mrkandreev.mapsmith.openaddressing.strategies; + +import java.util.Arrays; +import java.util.Objects; +import name.mrkandreev.mapsmith.LongObjectMap; +import name.mrkandreev.mapsmith.openaddressing.LongHashing; + +public final class LinearProbingLongObjectMap implements LongObjectMap { + private static final byte EMPTY = 0; + private static final byte OCCUPIED = 1; + private static final byte DELETED = 2; + + private long[] keys; + private Object[] values; + private byte[] states; + private int mask; + private int resizeThreshold; + private int entryCount; + private int used; + private final LongHashing hashing; + + public LinearProbingLongObjectMap() { + this(LongLongHashSupport.DEFAULT_EXPECTED_SIZE); + } + + public LinearProbingLongObjectMap(int expectedSize) { + this(expectedSize, LongHashing.MURMUR3_FINALIZER); + } + + public LinearProbingLongObjectMap(LongHashing hashing) { + this(LongLongHashSupport.DEFAULT_EXPECTED_SIZE, hashing); + } + + public LinearProbingLongObjectMap(int expectedSize, LongHashing hashing) { + this.hashing = Objects.requireNonNull(hashing, "hashing must not be null"); + allocate(LongLongHashSupport.capacityFor(expectedSize)); + } + + @Override + public int size() { + return entryCount; + } + + @Override + public boolean containsKey(long key) { + return findIndex(key) >= 0; + } + + @Override + public T get(long key) { + return getOrDefault(key, null); + } + + @Override + public T getOrDefault(long key, T defaultValue) { + int index = findIndex(key); + return index >= 0 ? valueAt(index) : defaultValue; + } + + @Override + public T put(long key, T value) { + ensureInsertCapacity(); + + int index = insertionIndex(key); + if (index >= 0) { + T previousValue = valueAt(index); + values[index] = value; + return previousValue; + } + + int insertionIndex = -index - 1; + if (states[insertionIndex] == EMPTY) { + used++; + } + states[insertionIndex] = OCCUPIED; + keys[insertionIndex] = key; + values[insertionIndex] = value; + entryCount++; + return null; + } + + @Override + public T remove(long key) { + int index = findIndex(key); + if (index < 0) { + return null; + } + + T previousValue = valueAt(index); + clearValue(index); + states[index] = DELETED; + entryCount--; + return previousValue; + } + + @Override + public void clear() { + Arrays.fill(states, EMPTY); + Arrays.fill(values, null); + entryCount = 0; + used = 0; + } + + private int findIndex(long key) { + int index = (int) hashing.hash(key) & mask; + while (true) { + byte state = states[index]; + if (state == EMPTY) { + return -1; + } + if (state == OCCUPIED && keys[index] == key) { + return index; + } + index = (index + 1) & mask; + } + } + + private int insertionIndex(long key) { + int index = (int) hashing.hash(key) & mask; + int firstDeletedIndex = -1; + + while (true) { + byte state = states[index]; + if (state == EMPTY) { + return firstDeletedIndex >= 0 ? -firstDeletedIndex - 1 : -index - 1; + } + if (state == OCCUPIED && keys[index] == key) { + return index; + } + if (state == DELETED && firstDeletedIndex < 0) { + firstDeletedIndex = index; + } + index = (index + 1) & mask; + } + } + + private void ensureInsertCapacity() { + if (used + 1 <= resizeThreshold) { + return; + } + + int nextCapacity = + entryCount + 1 <= resizeThreshold + ? keys.length + : LongLongHashSupport.nextCapacity(keys.length); + rehash(nextCapacity); + } + + private void rehash(int capacity) { + long[] oldKeys = keys; + Object[] oldValues = values; + byte[] oldStates = states; + + allocate(capacity); + + for (int i = 0; i < oldStates.length; i++) { + if (oldStates[i] == OCCUPIED) { + insertRehashed(oldKeys[i], valueFrom(oldValues[i])); + } + } + } + + private void insertRehashed(long key, T value) { + int index = (int) hashing.hash(key) & mask; + while (states[index] == OCCUPIED) { + index = (index + 1) & mask; + } + + states[index] = OCCUPIED; + keys[index] = key; + values[index] = value; + entryCount++; + used++; + } + + private void allocate(int capacity) { + keys = new long[capacity]; + values = new Object[capacity]; + states = new byte[capacity]; + mask = capacity - 1; + resizeThreshold = LongLongHashSupport.computeResizeThreshold(capacity); + entryCount = 0; + used = 0; + } + + @SuppressWarnings("unchecked") + private T valueAt(int index) { + return (T) values[index]; + } + + @SuppressWarnings("unchecked") + private T valueFrom(Object value) { + return (T) value; + } + + private void clearValue(int index) { + Arrays.fill(values, index, index + 1, null); + } +} diff --git a/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/strategies/LongLongHashSupport.java b/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/strategies/LongLongHashSupport.java new file mode 100644 index 0000000..5b37864 --- /dev/null +++ b/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/strategies/LongLongHashSupport.java @@ -0,0 +1,46 @@ +package name.mrkandreev.mapsmith.openaddressing.strategies; + +final class LongLongHashSupport { + static final int DEFAULT_EXPECTED_SIZE = 16; + static final int MIN_CAPACITY = 2; + static final int LOAD_FACTOR_NUMERATOR = 2; + static final int LOAD_FACTOR_DENOMINATOR = 3; + static final int MAX_CAPACITY = 1 << 30; + + private LongLongHashSupport() {} + + static int capacityFor(int expectedSize) { + if (expectedSize < 0) { + throw new IllegalArgumentException("expectedSize must be non-negative"); + } + if (expectedSize == 0) { + return MIN_CAPACITY; + } + + long capacity = ((long) expectedSize * LOAD_FACTOR_DENOMINATOR / LOAD_FACTOR_NUMERATOR) + 1L; + if (capacity > MAX_CAPACITY) { + throw new IllegalArgumentException("expectedSize is too large"); + } + + return tableSizeFor((int) capacity); + } + + static int nextCapacity(int capacity) { + if (capacity >= MAX_CAPACITY) { + throw new IllegalStateException("maximum capacity reached"); + } + return capacity << 1; + } + + static int computeResizeThreshold(int capacity) { + return Math.max(1, capacity * LOAD_FACTOR_NUMERATOR / LOAD_FACTOR_DENOMINATOR); + } + + private static int tableSizeFor(int capacity) { + int result = MIN_CAPACITY; + while (result < capacity) { + result <<= 1; + } + return result; + } +} diff --git a/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/strategies/RobinHoodLongLongMap.java b/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/strategies/RobinHoodLongLongMap.java new file mode 100644 index 0000000..7e311cf --- /dev/null +++ b/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/strategies/RobinHoodLongLongMap.java @@ -0,0 +1,189 @@ +package name.mrkandreev.mapsmith.openaddressing.strategies; + +import java.util.Arrays; +import java.util.Objects; +import name.mrkandreev.mapsmith.LongLongMap; +import name.mrkandreev.mapsmith.openaddressing.LongHashing; + +public final class RobinHoodLongLongMap implements LongLongMap { + private static final byte EMPTY = 0; + private static final byte OCCUPIED = 1; + + private long[] keys; + private long[] values; + private byte[] states; + private int mask; + private int resizeThreshold; + private int entryCount; + private final LongHashing hashing; + + public RobinHoodLongLongMap() { + this(LongLongHashSupport.DEFAULT_EXPECTED_SIZE); + } + + public RobinHoodLongLongMap(int expectedSize) { + this(expectedSize, LongHashing.MURMUR3_FINALIZER); + } + + public RobinHoodLongLongMap(LongHashing hashing) { + this(LongLongHashSupport.DEFAULT_EXPECTED_SIZE, hashing); + } + + public RobinHoodLongLongMap(int expectedSize, LongHashing hashing) { + this.hashing = Objects.requireNonNull(hashing, "hashing must not be null"); + allocate(LongLongHashSupport.capacityFor(expectedSize)); + } + + @Override + public int size() { + return entryCount; + } + + @Override + public boolean containsKey(long key) { + return findIndex(key) >= 0; + } + + @Override + public long get(long key) { + return getOrDefault(key, 0L); + } + + @Override + public long getOrDefault(long key, long defaultValue) { + int index = findIndex(key); + return index >= 0 ? values[index] : defaultValue; + } + + @Override + public long put(long key, long value) { + ensureInsertCapacity(); + return putWithoutResize(key, value); + } + + @Override + public long remove(long key) { + int index = findIndex(key); + if (index < 0) { + return 0L; + } + + long previousValue = values[index]; + backwardShift(index); + entryCount--; + return previousValue; + } + + @Override + public void clear() { + Arrays.fill(states, EMPTY); + entryCount = 0; + } + + private int findIndex(long key) { + long hash = hashing.hash(key); + int index = (int) hash & mask; + int distance = 0; + + while (true) { + if (states[index] == EMPTY) { + return -1; + } + if (keys[index] == key) { + return index; + } + if (probeDistance(index, keys[index]) < distance) { + return -1; + } + + index = (index + 1) & mask; + distance++; + } + } + + private long putWithoutResize(long key, long value) { + long currentKey = key; + long currentValue = value; + int index = (int) hashing.hash(currentKey) & mask; + int distance = 0; + + while (true) { + if (states[index] == EMPTY) { + states[index] = OCCUPIED; + keys[index] = currentKey; + values[index] = currentValue; + entryCount++; + return 0L; + } + if (keys[index] == currentKey) { + long previousValue = values[index]; + values[index] = currentValue; + return previousValue; + } + + int existingDistance = probeDistance(index, keys[index]); + if (existingDistance < distance) { + long swappedKey = keys[index]; + long swappedValue = values[index]; + keys[index] = currentKey; + values[index] = currentValue; + currentKey = swappedKey; + currentValue = swappedValue; + distance = existingDistance; + } + + index = (index + 1) & mask; + distance++; + } + } + + private void backwardShift(int deletedIndex) { + int index = deletedIndex; + int nextIndex = (index + 1) & mask; + + while (states[nextIndex] == OCCUPIED && probeDistance(nextIndex, keys[nextIndex]) > 0) { + keys[index] = keys[nextIndex]; + values[index] = values[nextIndex]; + states[index] = OCCUPIED; + index = nextIndex; + nextIndex = (nextIndex + 1) & mask; + } + + states[index] = EMPTY; + } + + private int probeDistance(int index, long key) { + int idealIndex = (int) hashing.hash(key) & mask; + return (index - idealIndex) & mask; + } + + private void ensureInsertCapacity() { + if (entryCount + 1 <= resizeThreshold) { + return; + } + rehash(LongLongHashSupport.nextCapacity(keys.length)); + } + + private void rehash(int capacity) { + long[] oldKeys = keys; + long[] oldValues = values; + byte[] oldStates = states; + + allocate(capacity); + + for (int i = 0; i < oldStates.length; i++) { + if (oldStates[i] == OCCUPIED) { + putWithoutResize(oldKeys[i], oldValues[i]); + } + } + } + + private void allocate(int capacity) { + keys = new long[capacity]; + values = new long[capacity]; + states = new byte[capacity]; + mask = capacity - 1; + resizeThreshold = LongLongHashSupport.computeResizeThreshold(capacity); + entryCount = 0; + } +} diff --git a/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/strategies/RobinHoodLongObjectMap.java b/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/strategies/RobinHoodLongObjectMap.java new file mode 100644 index 0000000..c638161 --- /dev/null +++ b/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/strategies/RobinHoodLongObjectMap.java @@ -0,0 +1,205 @@ +package name.mrkandreev.mapsmith.openaddressing.strategies; + +import java.util.Arrays; +import java.util.Objects; +import name.mrkandreev.mapsmith.LongObjectMap; +import name.mrkandreev.mapsmith.openaddressing.LongHashing; + +public final class RobinHoodLongObjectMap implements LongObjectMap { + private static final byte EMPTY = 0; + private static final byte OCCUPIED = 1; + + private long[] keys; + private Object[] values; + private byte[] states; + private int mask; + private int resizeThreshold; + private int entryCount; + private final LongHashing hashing; + + public RobinHoodLongObjectMap() { + this(LongLongHashSupport.DEFAULT_EXPECTED_SIZE); + } + + public RobinHoodLongObjectMap(int expectedSize) { + this(expectedSize, LongHashing.MURMUR3_FINALIZER); + } + + public RobinHoodLongObjectMap(LongHashing hashing) { + this(LongLongHashSupport.DEFAULT_EXPECTED_SIZE, hashing); + } + + public RobinHoodLongObjectMap(int expectedSize, LongHashing hashing) { + this.hashing = Objects.requireNonNull(hashing, "hashing must not be null"); + allocate(LongLongHashSupport.capacityFor(expectedSize)); + } + + @Override + public int size() { + return entryCount; + } + + @Override + public boolean containsKey(long key) { + return findIndex(key) >= 0; + } + + @Override + public T get(long key) { + return getOrDefault(key, null); + } + + @Override + public T getOrDefault(long key, T defaultValue) { + int index = findIndex(key); + return index >= 0 ? valueAt(index) : defaultValue; + } + + @Override + public T put(long key, T value) { + ensureInsertCapacity(); + return putWithoutResize(key, value); + } + + @Override + public T remove(long key) { + int index = findIndex(key); + if (index < 0) { + return null; + } + + T previousValue = valueAt(index); + backwardShift(index); + entryCount--; + return previousValue; + } + + @Override + public void clear() { + Arrays.fill(states, EMPTY); + Arrays.fill(values, null); + entryCount = 0; + } + + private int findIndex(long key) { + long hash = hashing.hash(key); + int index = (int) hash & mask; + int distance = 0; + + while (true) { + if (states[index] == EMPTY) { + return -1; + } + if (keys[index] == key) { + return index; + } + if (probeDistance(index, keys[index]) < distance) { + return -1; + } + + index = (index + 1) & mask; + distance++; + } + } + + private T putWithoutResize(long key, T value) { + long currentKey = key; + T currentValue = value; + int index = (int) hashing.hash(currentKey) & mask; + int distance = 0; + + while (true) { + if (states[index] == EMPTY) { + states[index] = OCCUPIED; + keys[index] = currentKey; + values[index] = currentValue; + entryCount++; + return null; + } + if (keys[index] == currentKey) { + T previousValue = valueAt(index); + values[index] = currentValue; + return previousValue; + } + + int existingDistance = probeDistance(index, keys[index]); + if (existingDistance < distance) { + long swappedKey = keys[index]; + T swappedValue = valueAt(index); + keys[index] = currentKey; + values[index] = currentValue; + currentKey = swappedKey; + currentValue = swappedValue; + distance = existingDistance; + } + + index = (index + 1) & mask; + distance++; + } + } + + private void backwardShift(int deletedIndex) { + int index = deletedIndex; + int nextIndex = (index + 1) & mask; + + while (states[nextIndex] == OCCUPIED && probeDistance(nextIndex, keys[nextIndex]) > 0) { + keys[index] = keys[nextIndex]; + values[index] = values[nextIndex]; + states[index] = OCCUPIED; + index = nextIndex; + nextIndex = (nextIndex + 1) & mask; + } + + clearValue(index); + states[index] = EMPTY; + } + + private int probeDistance(int index, long key) { + int idealIndex = (int) hashing.hash(key) & mask; + return (index - idealIndex) & mask; + } + + private void ensureInsertCapacity() { + if (entryCount + 1 <= resizeThreshold) { + return; + } + rehash(LongLongHashSupport.nextCapacity(keys.length)); + } + + private void rehash(int capacity) { + long[] oldKeys = keys; + Object[] oldValues = values; + byte[] oldStates = states; + + allocate(capacity); + + for (int i = 0; i < oldStates.length; i++) { + if (oldStates[i] == OCCUPIED) { + putWithoutResize(oldKeys[i], valueFrom(oldValues[i])); + } + } + } + + private void allocate(int capacity) { + keys = new long[capacity]; + values = new Object[capacity]; + states = new byte[capacity]; + mask = capacity - 1; + resizeThreshold = LongLongHashSupport.computeResizeThreshold(capacity); + entryCount = 0; + } + + @SuppressWarnings("unchecked") + private T valueAt(int index) { + return (T) values[index]; + } + + @SuppressWarnings("unchecked") + private T valueFrom(Object value) { + return (T) value; + } + + private void clearValue(int index) { + Arrays.fill(values, index, index + 1, null); + } +} diff --git a/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/strategies/SwissTableLongLongMap.java b/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/strategies/SwissTableLongLongMap.java new file mode 100644 index 0000000..0a9191a --- /dev/null +++ b/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/strategies/SwissTableLongLongMap.java @@ -0,0 +1,199 @@ +package name.mrkandreev.mapsmith.openaddressing.strategies; + +import java.util.Arrays; +import java.util.Objects; +import name.mrkandreev.mapsmith.LongLongMap; +import name.mrkandreev.mapsmith.openaddressing.LongHashing; + +public final class SwissTableLongLongMap implements LongLongMap { + private static final byte EMPTY = 0; + private static final byte DELETED = 1; + + private long[] keys; + private long[] values; + private byte[] controls; + private int mask; + private int resizeThreshold; + private int entryCount; + private int used; + private final LongHashing hashing; + + public SwissTableLongLongMap() { + this(LongLongHashSupport.DEFAULT_EXPECTED_SIZE); + } + + public SwissTableLongLongMap(int expectedSize) { + this(expectedSize, LongHashing.MURMUR3_FINALIZER); + } + + public SwissTableLongLongMap(LongHashing hashing) { + this(LongLongHashSupport.DEFAULT_EXPECTED_SIZE, hashing); + } + + public SwissTableLongLongMap(int expectedSize, LongHashing hashing) { + this.hashing = Objects.requireNonNull(hashing, "hashing must not be null"); + allocate(LongLongHashSupport.capacityFor(expectedSize)); + } + + @Override + public int size() { + return entryCount; + } + + @Override + public boolean containsKey(long key) { + return findIndex(key) >= 0; + } + + @Override + public long get(long key) { + return getOrDefault(key, 0L); + } + + @Override + public long getOrDefault(long key, long defaultValue) { + int index = findIndex(key); + return index >= 0 ? values[index] : defaultValue; + } + + @Override + public long put(long key, long value) { + ensureInsertCapacity(); + + long hash = hashing.hash(key); + byte fingerprint = fingerprint(hash); + int index = insertionIndex(key, hash, fingerprint); + if (index >= 0) { + long previousValue = values[index]; + values[index] = value; + return previousValue; + } + + int insertionIndex = -index - 1; + if (controls[insertionIndex] == EMPTY) { + used++; + } + controls[insertionIndex] = fingerprint; + keys[insertionIndex] = key; + values[insertionIndex] = value; + entryCount++; + return 0L; + } + + @Override + public long remove(long key) { + int index = findIndex(key); + if (index < 0) { + return 0L; + } + + long previousValue = values[index]; + controls[index] = DELETED; + entryCount--; + return previousValue; + } + + @Override + public void clear() { + Arrays.fill(controls, EMPTY); + entryCount = 0; + used = 0; + } + + private int findIndex(long key) { + long hash = hashing.hash(key); + byte fingerprint = fingerprint(hash); + int index = (int) hash & mask; + + while (true) { + byte control = controls[index]; + if (control == EMPTY) { + return -1; + } + if (control == fingerprint && keys[index] == key) { + return index; + } + index = (index + 1) & mask; + } + } + + private int insertionIndex(long key, long hash, byte fingerprint) { + int index = (int) hash & mask; + int firstDeletedIndex = -1; + + while (true) { + byte control = controls[index]; + if (control == EMPTY) { + return firstDeletedIndex >= 0 ? -firstDeletedIndex - 1 : -index - 1; + } + if (control == fingerprint && keys[index] == key) { + return index; + } + if (control == DELETED && firstDeletedIndex < 0) { + firstDeletedIndex = index; + } + index = (index + 1) & mask; + } + } + + private void ensureInsertCapacity() { + if (used + 1 <= resizeThreshold) { + return; + } + + int nextCapacity = + entryCount + 1 <= resizeThreshold + ? keys.length + : LongLongHashSupport.nextCapacity(keys.length); + rehash(nextCapacity); + } + + private void rehash(int capacity) { + long[] oldKeys = keys; + long[] oldValues = values; + byte[] oldControls = controls; + + allocate(capacity); + + for (int i = 0; i < oldControls.length; i++) { + if (isFull(oldControls[i])) { + insertRehashed(oldKeys[i], oldValues[i]); + } + } + } + + private void insertRehashed(long key, long value) { + long hash = hashing.hash(key); + byte fingerprint = fingerprint(hash); + int index = (int) hash & mask; + + while (isFull(controls[index])) { + index = (index + 1) & mask; + } + + controls[index] = fingerprint; + keys[index] = key; + values[index] = value; + entryCount++; + used++; + } + + private void allocate(int capacity) { + keys = new long[capacity]; + values = new long[capacity]; + controls = new byte[capacity]; + mask = capacity - 1; + resizeThreshold = LongLongHashSupport.computeResizeThreshold(capacity); + entryCount = 0; + used = 0; + } + + private static boolean isFull(byte control) { + return control != EMPTY && control != DELETED; + } + + private static byte fingerprint(long hash) { + int result = (int) (hash >>> 57) + 2; + return (byte) result; + } +} diff --git a/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/strategies/SwissTableLongObjectMap.java b/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/strategies/SwissTableLongObjectMap.java new file mode 100644 index 0000000..98d9f21 --- /dev/null +++ b/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/strategies/SwissTableLongObjectMap.java @@ -0,0 +1,215 @@ +package name.mrkandreev.mapsmith.openaddressing.strategies; + +import java.util.Arrays; +import java.util.Objects; +import name.mrkandreev.mapsmith.LongObjectMap; +import name.mrkandreev.mapsmith.openaddressing.LongHashing; + +public final class SwissTableLongObjectMap implements LongObjectMap { + private static final byte EMPTY = 0; + private static final byte DELETED = 1; + + private long[] keys; + private Object[] values; + private byte[] controls; + private int mask; + private int resizeThreshold; + private int entryCount; + private int used; + private final LongHashing hashing; + + public SwissTableLongObjectMap() { + this(LongLongHashSupport.DEFAULT_EXPECTED_SIZE); + } + + public SwissTableLongObjectMap(int expectedSize) { + this(expectedSize, LongHashing.MURMUR3_FINALIZER); + } + + public SwissTableLongObjectMap(LongHashing hashing) { + this(LongLongHashSupport.DEFAULT_EXPECTED_SIZE, hashing); + } + + public SwissTableLongObjectMap(int expectedSize, LongHashing hashing) { + this.hashing = Objects.requireNonNull(hashing, "hashing must not be null"); + allocate(LongLongHashSupport.capacityFor(expectedSize)); + } + + @Override + public int size() { + return entryCount; + } + + @Override + public boolean containsKey(long key) { + return findIndex(key) >= 0; + } + + @Override + public T get(long key) { + return getOrDefault(key, null); + } + + @Override + public T getOrDefault(long key, T defaultValue) { + int index = findIndex(key); + return index >= 0 ? valueAt(index) : defaultValue; + } + + @Override + public T put(long key, T value) { + ensureInsertCapacity(); + + long hash = hashing.hash(key); + byte fingerprint = fingerprint(hash); + int index = insertionIndex(key, hash, fingerprint); + if (index >= 0) { + T previousValue = valueAt(index); + values[index] = value; + return previousValue; + } + + int insertionIndex = -index - 1; + if (controls[insertionIndex] == EMPTY) { + used++; + } + controls[insertionIndex] = fingerprint; + keys[insertionIndex] = key; + values[insertionIndex] = value; + entryCount++; + return null; + } + + @Override + public T remove(long key) { + int index = findIndex(key); + if (index < 0) { + return null; + } + + T previousValue = valueAt(index); + clearValue(index); + controls[index] = DELETED; + entryCount--; + return previousValue; + } + + @Override + public void clear() { + Arrays.fill(controls, EMPTY); + Arrays.fill(values, null); + entryCount = 0; + used = 0; + } + + private int findIndex(long key) { + long hash = hashing.hash(key); + byte fingerprint = fingerprint(hash); + int index = (int) hash & mask; + + while (true) { + byte control = controls[index]; + if (control == EMPTY) { + return -1; + } + if (control == fingerprint && keys[index] == key) { + return index; + } + index = (index + 1) & mask; + } + } + + private int insertionIndex(long key, long hash, byte fingerprint) { + int index = (int) hash & mask; + int firstDeletedIndex = -1; + + while (true) { + byte control = controls[index]; + if (control == EMPTY) { + return firstDeletedIndex >= 0 ? -firstDeletedIndex - 1 : -index - 1; + } + if (control == fingerprint && keys[index] == key) { + return index; + } + if (control == DELETED && firstDeletedIndex < 0) { + firstDeletedIndex = index; + } + index = (index + 1) & mask; + } + } + + private void ensureInsertCapacity() { + if (used + 1 <= resizeThreshold) { + return; + } + + int nextCapacity = + entryCount + 1 <= resizeThreshold + ? keys.length + : LongLongHashSupport.nextCapacity(keys.length); + rehash(nextCapacity); + } + + private void rehash(int capacity) { + long[] oldKeys = keys; + Object[] oldValues = values; + byte[] oldControls = controls; + + allocate(capacity); + + for (int i = 0; i < oldControls.length; i++) { + if (isFull(oldControls[i])) { + insertRehashed(oldKeys[i], valueFrom(oldValues[i])); + } + } + } + + private void insertRehashed(long key, T value) { + long hash = hashing.hash(key); + byte fingerprint = fingerprint(hash); + int index = (int) hash & mask; + + while (isFull(controls[index])) { + index = (index + 1) & mask; + } + + controls[index] = fingerprint; + keys[index] = key; + values[index] = value; + entryCount++; + used++; + } + + private void allocate(int capacity) { + keys = new long[capacity]; + values = new Object[capacity]; + controls = new byte[capacity]; + mask = capacity - 1; + resizeThreshold = LongLongHashSupport.computeResizeThreshold(capacity); + entryCount = 0; + used = 0; + } + + private static boolean isFull(byte control) { + return control != EMPTY && control != DELETED; + } + + private static byte fingerprint(long hash) { + int result = (int) (hash >>> 57) + 2; + return (byte) result; + } + + @SuppressWarnings("unchecked") + private T valueAt(int index) { + return (T) values[index]; + } + + @SuppressWarnings("unchecked") + private T valueFrom(Object value) { + return (T) value; + } + + private void clearValue(int index) { + Arrays.fill(values, index, index + 1, null); + } +} diff --git a/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/range/LongBoundType.java b/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/range/LongBoundType.java new file mode 100644 index 0000000..9c8447f --- /dev/null +++ b/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/range/LongBoundType.java @@ -0,0 +1,6 @@ +package name.mrkandreev.mapsmith.range; + +public enum LongBoundType { + OPEN, + CLOSED +} diff --git a/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/range/LongLongRangeMap.java b/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/range/LongLongRangeMap.java new file mode 100644 index 0000000..2b13728 --- /dev/null +++ b/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/range/LongLongRangeMap.java @@ -0,0 +1,30 @@ +package name.mrkandreev.mapsmith.range; + +public interface LongLongRangeMap { + int size(); + + default boolean isEmpty() { + return size() == 0; + } + + boolean containsKey(long key); + + long get(long key); + + long getOrDefault(long key, long defaultValue); + + void put(long fromInclusive, long toInclusive, long value); + + void put(long lower, LongBoundType lowerType, long upper, LongBoundType upperType, long value); + + void putCoalescing(long fromInclusive, long toInclusive, long value); + + void putCoalescing( + long lower, LongBoundType lowerType, long upper, LongBoundType upperType, long value); + + void remove(long fromInclusive, long toInclusive); + + void remove(long lower, LongBoundType lowerType, long upper, LongBoundType upperType); + + void clear(); +} diff --git a/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/range/LongObjectRangeMap.java b/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/range/LongObjectRangeMap.java new file mode 100644 index 0000000..cb8cbe6 --- /dev/null +++ b/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/range/LongObjectRangeMap.java @@ -0,0 +1,30 @@ +package name.mrkandreev.mapsmith.range; + +public interface LongObjectRangeMap { + int size(); + + default boolean isEmpty() { + return size() == 0; + } + + boolean containsKey(long key); + + T get(long key); + + T getOrDefault(long key, T defaultValue); + + void put(long fromInclusive, long toInclusive, T value); + + void put(long lower, LongBoundType lowerType, long upper, LongBoundType upperType, T value); + + void putCoalescing(long fromInclusive, long toInclusive, T value); + + void putCoalescing( + long lower, LongBoundType lowerType, long upper, LongBoundType upperType, T value); + + void remove(long fromInclusive, long toInclusive); + + void remove(long lower, LongBoundType lowerType, long upper, LongBoundType upperType); + + void clear(); +} diff --git a/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/range/TreeLongLongRangeMap.java b/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/range/TreeLongLongRangeMap.java new file mode 100644 index 0000000..a589b44 --- /dev/null +++ b/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/range/TreeLongLongRangeMap.java @@ -0,0 +1,233 @@ +package name.mrkandreev.mapsmith.range; + +import java.util.Map; +import java.util.Objects; +import java.util.TreeMap; + +public final class TreeLongLongRangeMap implements LongLongRangeMap { + private final TreeMap ranges = new TreeMap<>(); + + @Override + public int size() { + return ranges.size(); + } + + @Override + public boolean containsKey(long key) { + return rangeFor(key) != null; + } + + @Override + public long get(long key) { + return getOrDefault(key, 0L); + } + + @Override + public long getOrDefault(long key, long defaultValue) { + Range range = rangeFor(key); + return range == null ? defaultValue : range.value; + } + + @Override + public void put(long fromInclusive, long toInclusive, long value) { + validateClosedRange(fromInclusive, toInclusive); + putClosed(fromInclusive, toInclusive, value, false); + } + + @Override + public void put( + long lower, LongBoundType lowerType, long upper, LongBoundType upperType, long value) { + putCanonical(lower, lowerType, upper, upperType, value, false); + } + + @Override + public void putCoalescing(long fromInclusive, long toInclusive, long value) { + validateClosedRange(fromInclusive, toInclusive); + putClosed(fromInclusive, toInclusive, value, true); + } + + @Override + public void putCoalescing( + long lower, LongBoundType lowerType, long upper, LongBoundType upperType, long value) { + putCanonical(lower, lowerType, upper, upperType, value, true); + } + + @Override + public void remove(long fromInclusive, long toInclusive) { + validateClosedRange(fromInclusive, toInclusive); + removeClosed(fromInclusive, toInclusive); + } + + @Override + public void remove(long lower, LongBoundType lowerType, long upper, LongBoundType upperType) { + removeCanonical(lower, lowerType, upper, upperType); + } + + private void removeClosed(long fromInclusive, long toInclusive) { + Map.Entry current = ranges.floorEntry(fromInclusive); + if (current == null || current.getValue().to < fromInclusive) { + current = ranges.ceilingEntry(fromInclusive); + } + + while (current != null) { + Range range = current.getValue(); + if (range.from > toInclusive) { + return; + } + + Map.Entry next = ranges.higherEntry(range.from); + ranges.remove(range.from); + + if (range.from < fromInclusive) { + ranges.put(range.from, new Range(range.from, fromInclusive - 1L, range.value)); + } + if (range.to > toInclusive) { + ranges.put(toInclusive + 1L, new Range(toInclusive + 1L, range.to, range.value)); + return; + } + + current = next; + } + } + + @Override + public void clear() { + ranges.clear(); + } + + private Range rangeFor(long key) { + Map.Entry entry = ranges.floorEntry(key); + if (entry == null) { + return null; + } + + Range range = entry.getValue(); + return range.to >= key ? range : null; + } + + private void putClosed(long fromInclusive, long toInclusive, long value, boolean coalesce) { + removeClosed(fromInclusive, toInclusive); + Range storedRange = new Range(fromInclusive, toInclusive, value); + if (coalesce) { + putCoalescing(storedRange); + } else { + ranges.put(storedRange.from, storedRange); + } + } + + private void putCanonical( + long lower, + LongBoundType lowerType, + long upper, + LongBoundType upperType, + long value, + boolean coalesce) { + Objects.requireNonNull(lowerType, "lowerType must not be null"); + Objects.requireNonNull(upperType, "upperType must not be null"); + validateTypedRange(lower, lowerType, upper, upperType); + + long fromInclusive = lower; + long toInclusive = upper; + if (lowerType == LongBoundType.OPEN) { + if (lower == Long.MAX_VALUE) { + return; + } + fromInclusive++; + } + if (upperType == LongBoundType.OPEN) { + if (upper == Long.MIN_VALUE) { + return; + } + toInclusive--; + } + + if (fromInclusive <= toInclusive) { + putClosed(fromInclusive, toInclusive, value, coalesce); + } + } + + private void removeCanonical( + long lower, LongBoundType lowerType, long upper, LongBoundType upperType) { + Objects.requireNonNull(lowerType, "lowerType must not be null"); + Objects.requireNonNull(upperType, "upperType must not be null"); + validateTypedRange(lower, lowerType, upper, upperType); + + long fromInclusive = lower; + long toInclusive = upper; + if (lowerType == LongBoundType.OPEN) { + if (lower == Long.MAX_VALUE) { + return; + } + fromInclusive++; + } + if (upperType == LongBoundType.OPEN) { + if (upper == Long.MIN_VALUE) { + return; + } + toInclusive--; + } + + if (fromInclusive <= toInclusive) { + removeClosed(fromInclusive, toInclusive); + } + } + + private void putCoalescing(Range range) { + Range merged = mergePrevious(range); + merged = mergeNext(merged); + ranges.put(merged.from, merged); + } + + private Range mergePrevious(Range range) { + Map.Entry previousEntry = ranges.lowerEntry(range.from); + if (previousEntry == null) { + return range; + } + + Range previous = previousEntry.getValue(); + if (previous.value != range.value || !touches(previous.to, range.from)) { + return range; + } + + ranges.remove(previous.from); + return new Range(previous.from, range.to, range.value); + } + + private Range mergeNext(Range range) { + Range result = range; + Map.Entry nextEntry = ranges.ceilingEntry(result.from); + while (nextEntry != null) { + Range next = nextEntry.getValue(); + if (next.value != result.value || !touches(result.to, next.from)) { + return result; + } + + ranges.remove(next.from); + result = new Range(result.from, next.to, result.value); + nextEntry = ranges.ceilingEntry(result.from); + } + return result; + } + + private static boolean touches(long leftTo, long rightFrom) { + return leftTo == Long.MAX_VALUE || leftTo + 1L >= rightFrom; + } + + private static void validateTypedRange( + long lower, LongBoundType lowerType, long upper, LongBoundType upperType) { + if (lower > upper) { + throw new IllegalArgumentException("lower must be <= upper"); + } + if (lower == upper && lowerType == LongBoundType.OPEN && upperType == LongBoundType.OPEN) { + throw new IllegalArgumentException("open range endpoints must be different"); + } + } + + private static void validateClosedRange(long fromInclusive, long toInclusive) { + if (fromInclusive > toInclusive) { + throw new IllegalArgumentException("fromInclusive must be <= toInclusive"); + } + } + + private record Range(long from, long to, long value) {} +} diff --git a/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/range/TreeLongObjectRangeMap.java b/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/range/TreeLongObjectRangeMap.java new file mode 100644 index 0000000..d066159 --- /dev/null +++ b/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/range/TreeLongObjectRangeMap.java @@ -0,0 +1,233 @@ +package name.mrkandreev.mapsmith.range; + +import java.util.Map; +import java.util.Objects; +import java.util.TreeMap; + +public final class TreeLongObjectRangeMap implements LongObjectRangeMap { + private final TreeMap> ranges = new TreeMap<>(); + + @Override + public int size() { + return ranges.size(); + } + + @Override + public boolean containsKey(long key) { + return rangeFor(key) != null; + } + + @Override + public T get(long key) { + return getOrDefault(key, null); + } + + @Override + public T getOrDefault(long key, T defaultValue) { + Range range = rangeFor(key); + return range == null ? defaultValue : range.value; + } + + @Override + public void put(long fromInclusive, long toInclusive, T value) { + validateClosedRange(fromInclusive, toInclusive); + putClosed(fromInclusive, toInclusive, value, false); + } + + @Override + public void put( + long lower, LongBoundType lowerType, long upper, LongBoundType upperType, T value) { + putCanonical(lower, lowerType, upper, upperType, value, false); + } + + @Override + public void putCoalescing(long fromInclusive, long toInclusive, T value) { + validateClosedRange(fromInclusive, toInclusive); + putClosed(fromInclusive, toInclusive, value, true); + } + + @Override + public void putCoalescing( + long lower, LongBoundType lowerType, long upper, LongBoundType upperType, T value) { + putCanonical(lower, lowerType, upper, upperType, value, true); + } + + @Override + public void remove(long fromInclusive, long toInclusive) { + validateClosedRange(fromInclusive, toInclusive); + removeClosed(fromInclusive, toInclusive); + } + + @Override + public void remove(long lower, LongBoundType lowerType, long upper, LongBoundType upperType) { + removeCanonical(lower, lowerType, upper, upperType); + } + + private void removeClosed(long fromInclusive, long toInclusive) { + Map.Entry> current = ranges.floorEntry(fromInclusive); + if (current == null || current.getValue().to < fromInclusive) { + current = ranges.ceilingEntry(fromInclusive); + } + + while (current != null) { + Range range = current.getValue(); + if (range.from > toInclusive) { + return; + } + + Map.Entry> next = ranges.higherEntry(range.from); + ranges.remove(range.from); + + if (range.from < fromInclusive) { + ranges.put(range.from, new Range<>(range.from, fromInclusive - 1L, range.value)); + } + if (range.to > toInclusive) { + ranges.put(toInclusive + 1L, new Range<>(toInclusive + 1L, range.to, range.value)); + return; + } + + current = next; + } + } + + @Override + public void clear() { + ranges.clear(); + } + + private Range rangeFor(long key) { + Map.Entry> entry = ranges.floorEntry(key); + if (entry == null) { + return null; + } + + Range range = entry.getValue(); + return range.to >= key ? range : null; + } + + private void putClosed(long fromInclusive, long toInclusive, T value, boolean coalesce) { + removeClosed(fromInclusive, toInclusive); + Range storedRange = new Range<>(fromInclusive, toInclusive, value); + if (coalesce) { + putCoalescing(storedRange); + } else { + ranges.put(storedRange.from, storedRange); + } + } + + private void putCanonical( + long lower, + LongBoundType lowerType, + long upper, + LongBoundType upperType, + T value, + boolean coalesce) { + Objects.requireNonNull(lowerType, "lowerType must not be null"); + Objects.requireNonNull(upperType, "upperType must not be null"); + validateTypedRange(lower, lowerType, upper, upperType); + + long fromInclusive = lower; + long toInclusive = upper; + if (lowerType == LongBoundType.OPEN) { + if (lower == Long.MAX_VALUE) { + return; + } + fromInclusive++; + } + if (upperType == LongBoundType.OPEN) { + if (upper == Long.MIN_VALUE) { + return; + } + toInclusive--; + } + + if (fromInclusive <= toInclusive) { + putClosed(fromInclusive, toInclusive, value, coalesce); + } + } + + private void removeCanonical( + long lower, LongBoundType lowerType, long upper, LongBoundType upperType) { + Objects.requireNonNull(lowerType, "lowerType must not be null"); + Objects.requireNonNull(upperType, "upperType must not be null"); + validateTypedRange(lower, lowerType, upper, upperType); + + long fromInclusive = lower; + long toInclusive = upper; + if (lowerType == LongBoundType.OPEN) { + if (lower == Long.MAX_VALUE) { + return; + } + fromInclusive++; + } + if (upperType == LongBoundType.OPEN) { + if (upper == Long.MIN_VALUE) { + return; + } + toInclusive--; + } + + if (fromInclusive <= toInclusive) { + removeClosed(fromInclusive, toInclusive); + } + } + + private void putCoalescing(Range range) { + Range merged = mergePrevious(range); + merged = mergeNext(merged); + ranges.put(merged.from, merged); + } + + private Range mergePrevious(Range range) { + Map.Entry> previousEntry = ranges.lowerEntry(range.from); + if (previousEntry == null) { + return range; + } + + Range previous = previousEntry.getValue(); + if (!Objects.equals(previous.value, range.value) || !touches(previous.to, range.from)) { + return range; + } + + ranges.remove(previous.from); + return new Range<>(previous.from, range.to, range.value); + } + + private Range mergeNext(Range range) { + Range result = range; + Map.Entry> nextEntry = ranges.ceilingEntry(result.from); + while (nextEntry != null) { + Range next = nextEntry.getValue(); + if (!Objects.equals(next.value, result.value) || !touches(result.to, next.from)) { + return result; + } + + ranges.remove(next.from); + result = new Range<>(result.from, next.to, result.value); + nextEntry = ranges.ceilingEntry(result.from); + } + return result; + } + + private static boolean touches(long leftTo, long rightFrom) { + return leftTo == Long.MAX_VALUE || leftTo + 1L >= rightFrom; + } + + private static void validateTypedRange( + long lower, LongBoundType lowerType, long upper, LongBoundType upperType) { + if (lower > upper) { + throw new IllegalArgumentException("lower must be <= upper"); + } + if (lower == upper && lowerType == LongBoundType.OPEN && upperType == LongBoundType.OPEN) { + throw new IllegalArgumentException("open range endpoints must be different"); + } + } + + private static void validateClosedRange(long fromInclusive, long toInclusive) { + if (fromInclusive > toInclusive) { + throw new IllegalArgumentException("fromInclusive must be <= toInclusive"); + } + } + + private record Range(long from, long to, T value) {} +} diff --git a/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/ranking/LongLongRankingMap.java b/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/ranking/LongLongRankingMap.java new file mode 100644 index 0000000..aff81c6 --- /dev/null +++ b/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/ranking/LongLongRankingMap.java @@ -0,0 +1,25 @@ +package name.mrkandreev.mapsmith.ranking; + +import name.mrkandreev.mapsmith.LongLongMap; + +public interface LongLongRankingMap extends LongLongMap { + int MISSING_RANK = -1; + + /** + * Returns a one-based rank for {@code key}. Higher values are ranked first. Keys with equal + * values are ranked by key in ascending order. + */ + int rankOf(long key); + + /** + * Returns how many entries are ranked before {@code key}, or {@link #MISSING_RANK} when the key + * is not present. + */ + int countBefore(long key); + + /** + * Returns how many entries are ranked after {@code key}, or {@link #MISSING_RANK} when the key is + * not present. + */ + int countAfter(long key); +} diff --git a/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/ranking/OrderStatisticLongLongMap.java b/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/ranking/OrderStatisticLongLongMap.java new file mode 100644 index 0000000..b8d3a11 --- /dev/null +++ b/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/ranking/OrderStatisticLongLongMap.java @@ -0,0 +1,228 @@ +package name.mrkandreev.mapsmith.ranking; + +import name.mrkandreev.mapsmith.LongLongMap; +import name.mrkandreev.mapsmith.openaddressing.LongLongOpenAddressMap; + +public final class OrderStatisticLongLongMap implements LongLongRankingMap { + private static final int MAX_BALANCE_DELTA = 1; + + private final LongLongMap scoreByKey; + private Node root; + + public OrderStatisticLongLongMap() { + this(LongLongOpenAddressMap.DEFAULT_EXPECTED_SIZE); + } + + public OrderStatisticLongLongMap(int expectedSize) { + scoreByKey = new LongLongOpenAddressMap(expectedSize); + } + + @Override + public int size() { + return scoreByKey.size(); + } + + @Override + public boolean containsKey(long key) { + return scoreByKey.containsKey(key); + } + + @Override + public long get(long key) { + return scoreByKey.get(key); + } + + @Override + public long getOrDefault(long key, long defaultValue) { + return scoreByKey.getOrDefault(key, defaultValue); + } + + @Override + public long put(long key, long value) { + boolean hadKey = scoreByKey.containsKey(key); + long previousValue = scoreByKey.put(key, value); + if (hadKey) { + root = delete(root, key, previousValue); + } + root = insert(root, key, value); + return previousValue; + } + + @Override + public long remove(long key) { + if (!scoreByKey.containsKey(key)) { + return 0L; + } + + long previousValue = scoreByKey.remove(key); + root = delete(root, key, previousValue); + return previousValue; + } + + @Override + @SuppressWarnings("PMD.NullAssignment") + public void clear() { + scoreByKey.clear(); + root = null; + } + + @Override + public int rankOf(long key) { + int countBefore = countBefore(key); + return countBefore == MISSING_RANK ? MISSING_RANK : countBefore + 1; + } + + @Override + public int countBefore(long key) { + if (!scoreByKey.containsKey(key)) { + return MISSING_RANK; + } + return countBefore(root, key, scoreByKey.get(key)); + } + + @Override + public int countAfter(long key) { + int countBefore = countBefore(key); + return countBefore == MISSING_RANK ? MISSING_RANK : size() - countBefore - 1; + } + + private static Node insert(Node node, long key, long value) { + if (node == null) { + return new Node(key, value); + } + + int comparison = compare(key, value, node); + if (comparison < 0) { + node.left = insert(node.left, key, value); + } else if (comparison > 0) { + node.right = insert(node.right, key, value); + } else { + node.value = value; + return node; + } + + return balance(update(node)); + } + + private static Node delete(Node node, long key, long value) { + if (node == null) { + return null; + } + + int comparison = compare(key, value, node); + if (comparison < 0) { + node.left = delete(node.left, key, value); + return balance(update(node)); + } + if (comparison > 0) { + node.right = delete(node.right, key, value); + return balance(update(node)); + } + + if (node.left == null) { + return node.right; + } + if (node.right == null) { + return node.left; + } + + Node successor = min(node.right); + successor.right = deleteMin(node.right); + successor.left = node.left; + return balance(update(successor)); + } + + private static Node deleteMin(Node node) { + if (node.left == null) { + return node.right; + } + node.left = deleteMin(node.left); + return balance(update(node)); + } + + private static Node min(Node node) { + Node current = node; + while (current.left != null) { + current = current.left; + } + return current; + } + + private static int countBefore(Node node, long key, long value) { + if (node == null) { + return 0; + } + + int comparison = compare(key, value, node); + if (comparison <= 0) { + return countBefore(node.left, key, value); + } + return size(node.left) + 1 + countBefore(node.right, key, value); + } + + private static Node balance(Node node) { + int balance = height(node.left) - height(node.right); + if (balance > MAX_BALANCE_DELTA) { + if (height(node.left.left) < height(node.left.right)) { + node.left = rotateLeft(node.left); + } + return rotateRight(node); + } + if (balance < -MAX_BALANCE_DELTA) { + if (height(node.right.right) < height(node.right.left)) { + node.right = rotateRight(node.right); + } + return rotateLeft(node); + } + return node; + } + + private static Node rotateLeft(Node node) { + Node right = node.right; + node.right = right.left; + right.left = update(node); + return update(right); + } + + private static Node rotateRight(Node node) { + Node left = node.left; + node.left = left.right; + left.right = update(node); + return update(left); + } + + private static Node update(Node node) { + node.height = Math.max(height(node.left), height(node.right)) + 1; + node.size = size(node.left) + size(node.right) + 1; + return node; + } + + private static int compare(long key, long value, Node node) { + if (value != node.value) { + return value > node.value ? -1 : 1; + } + return Long.compare(key, node.key); + } + + private static int height(Node node) { + return node == null ? 0 : node.height; + } + + private static int size(Node node) { + return node == null ? 0 : node.size; + } + + private static final class Node { + private final long key; + private long value; + private Node left; + private Node right; + private int height = 1; + private int size = 1; + + private Node(long key, long value) { + this.key = key; + this.value = value; + } + } +} diff --git a/mapsmith-core/src/test/java/name/mrkandreev/mapsmith/openaddressing/LongLongMapFactoryTest.java b/mapsmith-core/src/test/java/name/mrkandreev/mapsmith/openaddressing/LongLongMapFactoryTest.java new file mode 100644 index 0000000..354d391 --- /dev/null +++ b/mapsmith-core/src/test/java/name/mrkandreev/mapsmith/openaddressing/LongLongMapFactoryTest.java @@ -0,0 +1,95 @@ +package name.mrkandreev.mapsmith.openaddressing; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import name.mrkandreev.mapsmith.LongLongMap; +import name.mrkandreev.mapsmith.openaddressing.strategies.LinearProbingLongLongMap; +import org.junit.jupiter.api.Test; + +class LongLongMapFactoryTest { + @Test + void createsLinearProbingMaps() { + assertThat(LongLongMapFactory.create(MapSpecialization.LINEAR_PROBING)) + .isInstanceOf(LongLongOpenAddressMap.class); + assertThat(LongLongMapFactory.create(MapSpecialization.LINEAR_PROBING, 32)) + .isInstanceOf(LongLongOpenAddressMap.class); + } + + @Test + void createsRobinHoodMaps() { + assertThat(LongLongMapFactory.create(MapSpecialization.ROBIN_HOOD)) + .isInstanceOf(LongLongOpenAddressMap.class); + assertThat(LongLongMapFactory.create(MapSpecialization.ROBIN_HOOD, 32)) + .isInstanceOf(LongLongOpenAddressMap.class); + } + + @Test + void createsSwissTableMaps() { + assertThat(LongLongMapFactory.create(MapSpecialization.SWISS_TABLE)) + .isInstanceOf(LongLongOpenAddressMap.class); + assertThat(LongLongMapFactory.create(MapSpecialization.SWISS_TABLE, 32)) + .isInstanceOf(LongLongOpenAddressMap.class); + } + + @Test + void createdMapsCanStoreValues() { + LongLongMap map = + LongLongMapFactory.create(MapSpecialization.SWISS_TABLE, 32, LongHashing.FIBONACCI); + + assertThat(map.put(1L, 10L)).isZero(); + assertThat(map.get(1L)).isEqualTo(10L); + } + + @Test + void passesExpectedSizeToSpecialization() { + assertThatThrownBy(() -> LongLongMapFactory.create(MapSpecialization.LINEAR_PROBING, -1)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("expectedSize must be non-negative"); + } + + @Test + void openAddressMapAcceptsCustomStrategies() { + LongLongOpenAddressingStrategy strategy = + (expectedSize, hashing) -> { + assertThat(expectedSize).isEqualTo(32); + assertThat(hashing).isSameAs(LongHashing.IDENTITY); + return new LinearProbingLongLongMap(expectedSize, hashing); + }; + + LongLongMap map = new LongLongOpenAddressMap(strategy, 32, LongHashing.IDENTITY); + + assertThat(map.put(7L, 70L)).isZero(); + assertThat(map.get(7L)).isEqualTo(70L); + } + + @Test + void openAddressMapRejectsNullStrategy() { + assertThatThrownBy(() -> new LongLongOpenAddressMap(null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("strategy must not be null"); + } + + @Test + void openAddressMapRejectsNullStrategyResult() { + LongLongOpenAddressingStrategy strategy = (expectedSize, hashing) -> null; + + assertThatThrownBy(() -> new LongLongOpenAddressMap(strategy)) + .isInstanceOf(NullPointerException.class) + .hasMessage("strategy must not create a null map"); + } + + @Test + void rejectsNullSpecialization() { + assertThatThrownBy(() -> LongLongMapFactory.create((MapSpecialization) null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("specialization must not be null"); + } + + @Test + void rejectsNullHashing() { + assertThatThrownBy(() -> LongLongMapFactory.create(MapSpecialization.LINEAR_PROBING, null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("hashing must not be null"); + } +} diff --git a/mapsmith-core/src/test/java/name/mrkandreev/mapsmith/openaddressing/LongObjectMapFactoryTest.java b/mapsmith-core/src/test/java/name/mrkandreev/mapsmith/openaddressing/LongObjectMapFactoryTest.java new file mode 100644 index 0000000..416d262 --- /dev/null +++ b/mapsmith-core/src/test/java/name/mrkandreev/mapsmith/openaddressing/LongObjectMapFactoryTest.java @@ -0,0 +1,81 @@ +package name.mrkandreev.mapsmith.openaddressing; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import name.mrkandreev.mapsmith.LongObjectMap; +import name.mrkandreev.mapsmith.openaddressing.strategies.LinearProbingLongObjectMap; +import org.junit.jupiter.api.Test; + +class LongObjectMapFactoryTest { + @Test + void createsMaps() { + assertThat(LongObjectMapFactory.create(MapSpecialization.LINEAR_PROBING)) + .isInstanceOf(LongObjectOpenAddressMap.class); + assertThat(LongObjectMapFactory.create(MapSpecialization.ROBIN_HOOD, 32)) + .isInstanceOf(LongObjectOpenAddressMap.class); + assertThat(LongObjectMapFactory.create(MapSpecialization.SWISS_TABLE, LongHashing.FIBONACCI)) + .isInstanceOf(LongObjectOpenAddressMap.class); + } + + @Test + void createdMapsCanStoreValues() { + LongObjectMap map = + LongObjectMapFactory.create(MapSpecialization.SWISS_TABLE, 32, LongHashing.FIBONACCI); + + assertThat(map.put(1L, "one")).isNull(); + assertThat(map.get(1L)).isEqualTo("one"); + } + + @Test + void passesExpectedSizeToSpecialization() { + assertThatThrownBy(() -> LongObjectMapFactory.create(MapSpecialization.LINEAR_PROBING, -1)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("expectedSize must be non-negative"); + } + + @Test + void openAddressMapAcceptsCustomStrategies() { + LongObjectOpenAddressingStrategy strategy = + (expectedSize, hashing) -> { + assertThat(expectedSize).isEqualTo(32); + assertThat(hashing).isSameAs(LongHashing.IDENTITY); + return new LinearProbingLongObjectMap<>(expectedSize, hashing); + }; + + LongObjectMap map = new LongObjectOpenAddressMap<>(strategy, 32, LongHashing.IDENTITY); + + assertThat(map.put(7L, "seven")).isNull(); + assertThat(map.get(7L)).isEqualTo("seven"); + } + + @Test + void openAddressMapRejectsNullStrategy() { + assertThatThrownBy(() -> new LongObjectOpenAddressMap(null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("strategy must not be null"); + } + + @Test + void openAddressMapRejectsNullStrategyResult() { + LongObjectOpenAddressingStrategy strategy = (expectedSize, hashing) -> null; + + assertThatThrownBy(() -> new LongObjectOpenAddressMap(strategy)) + .isInstanceOf(NullPointerException.class) + .hasMessage("strategy must not create a null map"); + } + + @Test + void rejectsNullSpecialization() { + assertThatThrownBy(() -> LongObjectMapFactory.create((MapSpecialization) null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("specialization must not be null"); + } + + @Test + void rejectsNullHashing() { + assertThatThrownBy(() -> LongObjectMapFactory.create(MapSpecialization.LINEAR_PROBING, null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("hashing must not be null"); + } +} diff --git a/mapsmith-core/src/test/java/name/mrkandreev/mapsmith/openaddressing/strategies/LongLongMapImplementationTest.java b/mapsmith-core/src/test/java/name/mrkandreev/mapsmith/openaddressing/strategies/LongLongMapImplementationTest.java new file mode 100644 index 0000000..cc03542 --- /dev/null +++ b/mapsmith-core/src/test/java/name/mrkandreev/mapsmith/openaddressing/strategies/LongLongMapImplementationTest.java @@ -0,0 +1,243 @@ +package name.mrkandreev.mapsmith.openaddressing.strategies; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.SplittableRandom; +import java.util.function.IntFunction; +import java.util.stream.Stream; +import name.mrkandreev.mapsmith.LongLongMap; +import name.mrkandreev.mapsmith.openaddressing.LongHashing; +import name.mrkandreev.mapsmith.openaddressing.LongLongOpenAddressMap; +import name.mrkandreev.mapsmith.ranking.OrderStatisticLongLongMap; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class LongLongMapImplementationTest { + private static final String MAP_IMPLEMENTATIONS = "mapImplementations"; + + @ParameterizedTest + @MethodSource(MAP_IMPLEMENTATIONS) + void startsEmpty(String name, IntFunction maps) { + LongLongMap map = maps.apply(16); + + assertThat(map.size()).as(name).isZero(); + assertThat(map.isEmpty()).isTrue(); + assertThat(map.containsKey(10L)).isFalse(); + assertThat(map.get(10L)).isZero(); + assertThat(map.getOrDefault(10L, 42L)).isEqualTo(42L); + } + + @ParameterizedTest + @MethodSource(MAP_IMPLEMENTATIONS) + void rejectsNegativeExpectedSize(String name, IntFunction maps) { + assertThatThrownBy(() -> maps.apply(-1)) + .as(name) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("expectedSize must be non-negative"); + } + + @ParameterizedTest + @MethodSource(MAP_IMPLEMENTATIONS) + void putsAndGetsValues(String name, IntFunction maps) { + LongLongMap map = maps.apply(16); + + assertThat(map.put(10L, 100L)).as(name).isZero(); + assertThat(map.put(-1L, -100L)).isZero(); + + assertThat(map.size()).isEqualTo(2); + assertThat(map.isEmpty()).isFalse(); + assertThat(map.containsKey(10L)).isTrue(); + assertThat(map.containsKey(-1L)).isTrue(); + assertThat(map.get(10L)).isEqualTo(100L); + assertThat(map.get(-1L)).isEqualTo(-100L); + } + + @ParameterizedTest + @MethodSource(MAP_IMPLEMENTATIONS) + void returnsPreviousValueWhenUpdating(String name, IntFunction maps) { + LongLongMap map = maps.apply(16); + + assertThat(map.put(7L, 70L)).as(name).isZero(); + assertThat(map.put(7L, 700L)).isEqualTo(70L); + + assertThat(map.size()).isEqualTo(1); + assertThat(map.get(7L)).isEqualTo(700L); + } + + @ParameterizedTest + @MethodSource(MAP_IMPLEMENTATIONS) + void supportsZeroAsKeyAndValue(String name, IntFunction maps) { + LongLongMap map = maps.apply(16); + + assertThat(map.put(0L, 0L)).as(name).isZero(); + + assertThat(map.containsKey(0L)).isTrue(); + assertThat(map.get(0L)).isZero(); + assertThat(map.getOrDefault(0L, 99L)).isZero(); + } + + @ParameterizedTest + @MethodSource(MAP_IMPLEMENTATIONS) + void removesValues(String name, IntFunction maps) { + LongLongMap map = maps.apply(16); + map.put(1L, 10L); + map.put(2L, 20L); + + assertThat(map.remove(1L)).as(name).isEqualTo(10L); + + assertThat(map.size()).isEqualTo(1); + assertThat(map.containsKey(1L)).isFalse(); + assertThat(map.containsKey(2L)).isTrue(); + assertThat(map.remove(1L)).isZero(); + } + + @ParameterizedTest + @MethodSource(MAP_IMPLEMENTATIONS) + void keepsProbeChainSearchableAfterRemove(String name, IntFunction maps) { + LongLongMap map = maps.apply(1); + + for (long key = 0; key < 100; key++) { + map.put(key, key * 10L); + } + for (long key = 0; key < 100; key += 2) { + assertThat(map.remove(key)).as(name).isEqualTo(key * 10L); + } + + for (long key = 1; key < 100; key += 2) { + assertThat(map.containsKey(key)).isTrue(); + assertThat(map.get(key)).isEqualTo(key * 10L); + } + } + + @ParameterizedTest + @MethodSource(MAP_IMPLEMENTATIONS) + void reusesDeletedOrShiftedSlots(String name, IntFunction maps) { + LongLongMap map = maps.apply(1); + + for (long key = 0; key < 50; key++) { + map.put(key, key); + } + for (long key = 0; key < 50; key++) { + assertThat(map.remove(key)).as(name).isEqualTo(key); + } + for (long key = 50; key < 100; key++) { + map.put(key, key * 2L); + } + + assertThat(map.size()).isEqualTo(50); + for (long key = 50; key < 100; key++) { + assertThat(map.get(key)).isEqualTo(key * 2L); + } + } + + @ParameterizedTest + @MethodSource(MAP_IMPLEMENTATIONS) + void growsWhenLoadThresholdIsReached(String name, IntFunction maps) { + LongLongMap map = maps.apply(1); + + for (long key = 0; key < 10_000; key++) { + assertThat(map.put(key, key + 1L)).as(name).isZero(); + } + + assertThat(map.size()).isEqualTo(10_000); + for (long key = 0; key < 10_000; key++) { + assertThat(map.get(key)).isEqualTo(key + 1L); + } + } + + @ParameterizedTest + @MethodSource(MAP_IMPLEMENTATIONS) + void clearsValues(String name, IntFunction maps) { + LongLongMap map = maps.apply(16); + map.put(1L, 10L); + map.put(2L, 20L); + + map.clear(); + + assertThat(map.size()).as(name).isZero(); + assertThat(map.isEmpty()).isTrue(); + assertThat(map.containsKey(1L)).isFalse(); + assertThat(map.containsKey(2L)).isFalse(); + } + + @ParameterizedTest + @MethodSource(MAP_IMPLEMENTATIONS) + void matchesHashMapForRandomOperations(String name, IntFunction maps) { + LongLongMap map = maps.apply(1); + Map expected = new HashMap<>(); + SplittableRandom random = new SplittableRandom(0x6d6170736d697468L); + + for (int i = 0; i < 20_000; i++) { + long key = random.nextLong(2_000L); + long value = random.nextLong(); + int operation = random.nextInt(3); + + switch (operation) { + case 0 -> { + Long previousValue = expected.put(key, value); + assertThat(map.put(key, value)) + .as(name) + .isEqualTo(previousValue == null ? 0L : previousValue); + } + case 1 -> { + Long previousValue = expected.remove(key); + assertThat(map.remove(key)) + .as(name) + .isEqualTo(previousValue == null ? 0L : previousValue); + } + default -> { + assertThat(map.containsKey(key)).as(name).isEqualTo(expected.containsKey(key)); + assertThat(map.getOrDefault(key, Long.MIN_VALUE)) + .isEqualTo(expected.getOrDefault(key, Long.MIN_VALUE)); + } + } + + assertThat(map.size()).as(name).isEqualTo(expected.size()); + } + } + + private static Stream mapImplementations() { + return Arrays.stream(LongHashing.values()) + .flatMap( + hashing -> + Stream.of( + Arguments.of( + "linear probing " + hashing, + (IntFunction) + expectedSize -> new LinearProbingLongLongMap(expectedSize, hashing)), + Arguments.of( + "robin hood " + hashing, + (IntFunction) + expectedSize -> new RobinHoodLongLongMap(expectedSize, hashing)), + Arguments.of( + "swiss table " + hashing, + (IntFunction) + expectedSize -> new SwissTableLongLongMap(expectedSize, hashing)), + Arguments.of( + "open address linear probing " + hashing, + (IntFunction) + expectedSize -> + new LongLongOpenAddressMap( + LinearProbingLongLongMap::new, expectedSize, hashing)), + Arguments.of( + "open address robin hood " + hashing, + (IntFunction) + expectedSize -> + new LongLongOpenAddressMap( + RobinHoodLongLongMap::new, expectedSize, hashing)), + Arguments.of( + "open address swiss table " + hashing, + (IntFunction) + expectedSize -> + new LongLongOpenAddressMap( + SwissTableLongLongMap::new, expectedSize, hashing)), + Arguments.of( + "order statistic " + hashing, + (IntFunction) OrderStatisticLongLongMap::new))); + } +} diff --git a/mapsmith-core/src/test/java/name/mrkandreev/mapsmith/openaddressing/strategies/LongObjectMapImplementationTest.java b/mapsmith-core/src/test/java/name/mrkandreev/mapsmith/openaddressing/strategies/LongObjectMapImplementationTest.java new file mode 100644 index 0000000..94f0f08 --- /dev/null +++ b/mapsmith-core/src/test/java/name/mrkandreev/mapsmith/openaddressing/strategies/LongObjectMapImplementationTest.java @@ -0,0 +1,210 @@ +package name.mrkandreev.mapsmith.openaddressing.strategies; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.SplittableRandom; +import java.util.function.IntFunction; +import java.util.stream.Stream; +import name.mrkandreev.mapsmith.LongObjectMap; +import name.mrkandreev.mapsmith.openaddressing.LongHashing; +import name.mrkandreev.mapsmith.openaddressing.LongObjectOpenAddressMap; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class LongObjectMapImplementationTest { + private static final String MAP_IMPLEMENTATIONS = "mapImplementations"; + private static final String VALUE_PREFIX = "value-"; + + @ParameterizedTest + @MethodSource(MAP_IMPLEMENTATIONS) + void startsEmpty(String name, IntFunction> maps) { + LongObjectMap map = maps.apply(16); + + assertThat(map.size()).as(name).isZero(); + assertThat(map.isEmpty()).isTrue(); + assertThat(map.containsKey(10L)).isFalse(); + assertThat(map.get(10L)).isNull(); + assertThat(map.getOrDefault(10L, "fallback")).isEqualTo("fallback"); + } + + @ParameterizedTest + @MethodSource(MAP_IMPLEMENTATIONS) + void rejectsNegativeExpectedSize(String name, IntFunction> maps) { + assertThatThrownBy(() -> maps.apply(-1)) + .as(name) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("expectedSize must be non-negative"); + } + + @ParameterizedTest + @MethodSource(MAP_IMPLEMENTATIONS) + void putsAndGetsValues(String name, IntFunction> maps) { + LongObjectMap map = maps.apply(16); + + assertThat(map.put(10L, "ten")).as(name).isNull(); + assertThat(map.put(-1L, "minus-one")).isNull(); + + assertThat(map.size()).isEqualTo(2); + assertThat(map.isEmpty()).isFalse(); + assertThat(map.containsKey(10L)).isTrue(); + assertThat(map.containsKey(-1L)).isTrue(); + assertThat(map.get(10L)).isEqualTo("ten"); + assertThat(map.get(-1L)).isEqualTo("minus-one"); + } + + @ParameterizedTest + @MethodSource(MAP_IMPLEMENTATIONS) + void returnsPreviousValueWhenUpdating(String name, IntFunction> maps) { + LongObjectMap map = maps.apply(16); + + assertThat(map.put(7L, "seven")).as(name).isNull(); + assertThat(map.put(7L, "updated")).isEqualTo("seven"); + + assertThat(map.size()).isEqualTo(1); + assertThat(map.get(7L)).isEqualTo("updated"); + } + + @ParameterizedTest + @MethodSource(MAP_IMPLEMENTATIONS) + void supportsNullValues(String name, IntFunction> maps) { + LongObjectMap map = maps.apply(16); + + assertThat(map.put(0L, null)).as(name).isNull(); + + assertThat(map.containsKey(0L)).isTrue(); + assertThat(map.get(0L)).isNull(); + assertThat(map.getOrDefault(0L, "fallback")).isNull(); + } + + @ParameterizedTest + @MethodSource(MAP_IMPLEMENTATIONS) + void removesValues(String name, IntFunction> maps) { + LongObjectMap map = maps.apply(16); + map.put(1L, "one"); + map.put(2L, "two"); + + assertThat(map.remove(1L)).as(name).isEqualTo("one"); + + assertThat(map.size()).isEqualTo(1); + assertThat(map.containsKey(1L)).isFalse(); + assertThat(map.containsKey(2L)).isTrue(); + assertThat(map.remove(1L)).isNull(); + } + + @ParameterizedTest + @MethodSource(MAP_IMPLEMENTATIONS) + void keepsProbeChainSearchableAfterRemove(String name, IntFunction> maps) { + LongObjectMap map = maps.apply(1); + + for (long key = 0; key < 100; key++) { + map.put(key, VALUE_PREFIX + key); + } + for (long key = 0; key < 100; key += 2) { + assertThat(map.remove(key)).as(name).isEqualTo(VALUE_PREFIX + key); + } + + for (long key = 1; key < 100; key += 2) { + assertThat(map.containsKey(key)).isTrue(); + assertThat(map.get(key)).isEqualTo(VALUE_PREFIX + key); + } + } + + @ParameterizedTest + @MethodSource(MAP_IMPLEMENTATIONS) + void growsWhenLoadThresholdIsReached(String name, IntFunction> maps) { + LongObjectMap map = maps.apply(1); + + for (long key = 0; key < 10_000; key++) { + assertThat(map.put(key, VALUE_PREFIX + key)).as(name).isNull(); + } + + assertThat(map.size()).isEqualTo(10_000); + for (long key = 0; key < 10_000; key++) { + assertThat(map.get(key)).isEqualTo(VALUE_PREFIX + key); + } + } + + @ParameterizedTest + @MethodSource(MAP_IMPLEMENTATIONS) + void clearsValues(String name, IntFunction> maps) { + LongObjectMap map = maps.apply(16); + map.put(1L, "one"); + map.put(2L, "two"); + + map.clear(); + + assertThat(map.size()).as(name).isZero(); + assertThat(map.isEmpty()).isTrue(); + assertThat(map.containsKey(1L)).isFalse(); + assertThat(map.containsKey(2L)).isFalse(); + } + + @ParameterizedTest + @MethodSource(MAP_IMPLEMENTATIONS) + void matchesHashMapForRandomOperations(String name, IntFunction> maps) { + LongObjectMap map = maps.apply(1); + Map expected = new HashMap<>(); + SplittableRandom random = new SplittableRandom(0x6d6170736d697468L); + + for (int i = 0; i < 20_000; i++) { + long key = random.nextLong(2_000L); + String value = VALUE_PREFIX + random.nextLong(); + int operation = random.nextInt(3); + + switch (operation) { + case 0 -> assertThat(map.put(key, value)).as(name).isEqualTo(expected.put(key, value)); + case 1 -> assertThat(map.remove(key)).as(name).isEqualTo(expected.remove(key)); + default -> { + assertThat(map.containsKey(key)).as(name).isEqualTo(expected.containsKey(key)); + assertThat(map.getOrDefault(key, "missing")) + .isEqualTo(expected.getOrDefault(key, "missing")); + } + } + + assertThat(map.size()).as(name).isEqualTo(expected.size()); + } + } + + private static Stream mapImplementations() { + return Arrays.stream(LongHashing.values()) + .flatMap( + hashing -> + Stream.of( + Arguments.of( + "linear probing " + hashing, + (IntFunction>) + expectedSize -> + new LinearProbingLongObjectMap<>(expectedSize, hashing)), + Arguments.of( + "robin hood " + hashing, + (IntFunction>) + expectedSize -> new RobinHoodLongObjectMap<>(expectedSize, hashing)), + Arguments.of( + "swiss table " + hashing, + (IntFunction>) + expectedSize -> new SwissTableLongObjectMap<>(expectedSize, hashing)), + Arguments.of( + "open address linear probing " + hashing, + (IntFunction>) + expectedSize -> + new LongObjectOpenAddressMap<>( + LinearProbingLongObjectMap::new, expectedSize, hashing)), + Arguments.of( + "open address robin hood " + hashing, + (IntFunction>) + expectedSize -> + new LongObjectOpenAddressMap<>( + RobinHoodLongObjectMap::new, expectedSize, hashing)), + Arguments.of( + "open address swiss table " + hashing, + (IntFunction>) + expectedSize -> + new LongObjectOpenAddressMap<>( + SwissTableLongObjectMap::new, expectedSize, hashing)))); + } +} diff --git a/mapsmith-core/src/test/java/name/mrkandreev/mapsmith/range/TreeLongLongRangeMapTest.java b/mapsmith-core/src/test/java/name/mrkandreev/mapsmith/range/TreeLongLongRangeMapTest.java new file mode 100644 index 0000000..f2f4b4c --- /dev/null +++ b/mapsmith-core/src/test/java/name/mrkandreev/mapsmith/range/TreeLongLongRangeMapTest.java @@ -0,0 +1,203 @@ +package name.mrkandreev.mapsmith.range; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.HashMap; +import java.util.Map; +import java.util.SplittableRandom; +import org.junit.jupiter.api.Test; + +class TreeLongLongRangeMapTest { + @Test + void startsEmpty() { + LongLongRangeMap map = new TreeLongLongRangeMap(); + + assertThat(map.size()).isZero(); + assertThat(map.isEmpty()).isTrue(); + assertThat(map.containsKey(10L)).isFalse(); + assertThat(map.get(10L)).isZero(); + assertThat(map.getOrDefault(10L, 42L)).isEqualTo(42L); + } + + @Test + void storesAndFindsRanges() { + LongLongRangeMap map = new TreeLongLongRangeMap(); + + map.put(10L, 20L, 100L); + + assertThat(map.size()).isEqualTo(1); + assertThat(map.containsKey(9L)).isFalse(); + assertThat(map.containsKey(10L)).isTrue(); + assertThat(map.containsKey(20L)).isTrue(); + assertThat(map.containsKey(21L)).isFalse(); + assertThat(map.get(15L)).isEqualTo(100L); + } + + @Test + void singletonRangeOverwritesSingleKey() { + LongLongRangeMap map = new TreeLongLongRangeMap(); + map.put(10L, 20L, 100L); + + map.put(15L, 15L, 200L); + + assertThat(map.size()).isEqualTo(3); + assertThat(map.get(14L)).isEqualTo(100L); + assertThat(map.get(15L)).isEqualTo(200L); + assertThat(map.get(16L)).isEqualTo(100L); + } + + @Test + void rangePutOverwritesAndSplitsOverlaps() { + LongLongRangeMap map = new TreeLongLongRangeMap(); + map.put(0L, 9L, 10L); + map.put(20L, 29L, 20L); + + map.put(5L, 24L, 99L); + + assertThat(map.size()).isEqualTo(3); + assertThat(map.get(4L)).isEqualTo(10L); + assertThat(map.get(5L)).isEqualTo(99L); + assertThat(map.get(24L)).isEqualTo(99L); + assertThat(map.get(25L)).isEqualTo(20L); + } + + @Test + void putDoesNotCoalesceAdjacentRangesWithSameValue() { + LongLongRangeMap map = new TreeLongLongRangeMap(); + + map.put(0L, 9L, 10L); + map.put(10L, 19L, 10L); + map.put(20L, 29L, 10L); + + assertThat(map.size()).isEqualTo(3); + assertThat(map.get(0L)).isEqualTo(10L); + assertThat(map.get(29L)).isEqualTo(10L); + } + + @Test + void putCoalescingMergesAdjacentRangesWithSameValue() { + LongLongRangeMap map = new TreeLongLongRangeMap(); + + map.putCoalescing(0L, 9L, 10L); + map.putCoalescing(10L, 19L, 10L); + map.putCoalescing(20L, 29L, 10L); + + assertThat(map.size()).isEqualTo(1); + assertThat(map.get(0L)).isEqualTo(10L); + assertThat(map.get(29L)).isEqualTo(10L); + } + + @Test + void removesSingleKeysAndRanges() { + LongLongRangeMap map = new TreeLongLongRangeMap(); + map.put(0L, 99L, 10L); + + map.remove(50L, 50L); + map.remove(20L, 29L); + + assertThat(map.get(19L)).isEqualTo(10L); + assertThat(map.containsKey(20L)).isFalse(); + assertThat(map.containsKey(29L)).isFalse(); + assertThat(map.get(30L)).isEqualTo(10L); + assertThat(map.containsKey(50L)).isFalse(); + assertThat(map.get(51L)).isEqualTo(10L); + } + + @Test + void supportsLongExtremes() { + LongLongRangeMap map = new TreeLongLongRangeMap(); + + map.put(Long.MIN_VALUE, Long.MIN_VALUE, 1L); + map.put(Long.MAX_VALUE, Long.MAX_VALUE, 2L); + + assertThat(map.get(Long.MIN_VALUE)).isEqualTo(1L); + assertThat(map.get(Long.MAX_VALUE)).isEqualTo(2L); + } + + @Test + void supportsOpenClosedAndUnboundedRanges() { + LongLongRangeMap map = new TreeLongLongRangeMap(); + + map.put(10L, LongBoundType.OPEN, 20L, LongBoundType.CLOSED, 1L); + map.put(Long.MIN_VALUE, LongBoundType.CLOSED, 0L, LongBoundType.OPEN, 2L); + map.put(100L, LongBoundType.OPEN, Long.MAX_VALUE, LongBoundType.CLOSED, 3L); + + assertThat(map.containsKey(10L)).isFalse(); + assertThat(map.get(11L)).isEqualTo(1L); + assertThat(map.get(20L)).isEqualTo(1L); + assertThat(map.get(-1L)).isEqualTo(2L); + assertThat(map.containsKey(0L)).isFalse(); + assertThat(map.containsKey(100L)).isFalse(); + assertThat(map.get(101L)).isEqualTo(3L); + } + + @Test + void ignoresEmptyDiscreteRanges() { + LongLongRangeMap map = new TreeLongLongRangeMap(); + + map.put(10L, LongBoundType.CLOSED, 10L, LongBoundType.OPEN, 1L); + map.remove(10L, LongBoundType.CLOSED, 10L, LongBoundType.OPEN); + + assertThat(map.isEmpty()).isTrue(); + } + + @Test + void rejectsInvalidRangesAndNullBoundTypes() { + LongLongRangeMap map = new TreeLongLongRangeMap(); + + assertThatThrownBy(() -> map.put(2L, 1L, 10L)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("fromInclusive must be <= toInclusive"); + assertThatThrownBy(() -> map.remove(2L, 1L)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("fromInclusive must be <= toInclusive"); + assertThatThrownBy(() -> map.put(1L, null, 2L, LongBoundType.CLOSED, 10L)) + .isInstanceOf(NullPointerException.class) + .hasMessage("lowerType must not be null"); + assertThatThrownBy(() -> map.putCoalescing(1L, LongBoundType.CLOSED, 2L, null, 10L)) + .isInstanceOf(NullPointerException.class) + .hasMessage("upperType must not be null"); + assertThatThrownBy(() -> map.remove(1L, LongBoundType.OPEN, 1L, LongBoundType.OPEN)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("open range endpoints must be different"); + assertThatThrownBy(() -> map.remove(2L, LongBoundType.CLOSED, 1L, LongBoundType.CLOSED)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("lower must be <= upper"); + assertThatThrownBy(() -> map.remove(1L, null, 2L, LongBoundType.CLOSED)) + .isInstanceOf(NullPointerException.class) + .hasMessage("lowerType must not be null"); + } + + @Test + void matchesPointModelForRandomOperations() { + LongLongRangeMap map = new TreeLongLongRangeMap(); + Map expected = new HashMap<>(); + SplittableRandom random = new SplittableRandom(0x72616e67656d6170L); + + for (int i = 0; i < 10_000; i++) { + long from = random.nextLong(-50L, 51L); + long to = random.nextLong(from, 51L); + int operation = random.nextInt(3); + + if (operation == 0) { + map.remove(from, to); + for (long key = from; key <= to; key++) { + expected.remove(key); + } + } else { + long value = random.nextLong(); + map.put(from, to, value); + for (long key = from; key <= to; key++) { + expected.put(key, value); + } + } + + for (long key = -50L; key <= 50L; key++) { + assertThat(map.containsKey(key)).isEqualTo(expected.containsKey(key)); + assertThat(map.getOrDefault(key, Long.MIN_VALUE)) + .isEqualTo(expected.getOrDefault(key, Long.MIN_VALUE)); + } + } + } +} diff --git a/mapsmith-core/src/test/java/name/mrkandreev/mapsmith/range/TreeLongObjectRangeMapTest.java b/mapsmith-core/src/test/java/name/mrkandreev/mapsmith/range/TreeLongObjectRangeMapTest.java new file mode 100644 index 0000000..06668d2 --- /dev/null +++ b/mapsmith-core/src/test/java/name/mrkandreev/mapsmith/range/TreeLongObjectRangeMapTest.java @@ -0,0 +1,168 @@ +package name.mrkandreev.mapsmith.range; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.HashMap; +import java.util.Map; +import java.util.SplittableRandom; +import org.junit.jupiter.api.Test; + +class TreeLongObjectRangeMapTest { + private static final String SAME_VALUE = "same"; + private static final String STORED_VALUE = "value"; + + @Test + void startsEmpty() { + LongObjectRangeMap map = new TreeLongObjectRangeMap<>(); + + assertThat(map.size()).isZero(); + assertThat(map.isEmpty()).isTrue(); + assertThat(map.containsKey(10L)).isFalse(); + assertThat(map.get(10L)).isNull(); + assertThat(map.getOrDefault(10L, "fallback")).isEqualTo("fallback"); + } + + @Test + void storesAndFindsRanges() { + LongObjectRangeMap map = new TreeLongObjectRangeMap<>(); + + map.put(10L, 20L, "tier-a"); + + assertThat(map.size()).isEqualTo(1); + assertThat(map.containsKey(9L)).isFalse(); + assertThat(map.containsKey(10L)).isTrue(); + assertThat(map.containsKey(20L)).isTrue(); + assertThat(map.containsKey(21L)).isFalse(); + assertThat(map.get(15L)).isEqualTo("tier-a"); + } + + @Test + void rangePutOverwritesAndSplitsOverlaps() { + LongObjectRangeMap map = new TreeLongObjectRangeMap<>(); + map.put(0L, 9L, "left"); + map.put(20L, 29L, "right"); + + map.put(5L, 24L, "middle"); + + assertThat(map.size()).isEqualTo(3); + assertThat(map.get(4L)).isEqualTo("left"); + assertThat(map.get(5L)).isEqualTo("middle"); + assertThat(map.get(24L)).isEqualTo("middle"); + assertThat(map.get(25L)).isEqualTo("right"); + } + + @Test + void putCoalescingMergesAdjacentRangesWithEqualValues() { + LongObjectRangeMap map = new TreeLongObjectRangeMap<>(); + + map.putCoalescing(0L, 9L, sameValueCopy()); + map.putCoalescing(10L, 19L, sameValueCopy()); + map.putCoalescing(20L, 29L, SAME_VALUE); + + assertThat(map.size()).isEqualTo(1); + assertThat(map.get(0L)).isEqualTo(SAME_VALUE); + assertThat(map.get(29L)).isEqualTo(SAME_VALUE); + } + + @Test + void supportsNullValues() { + LongObjectRangeMap map = new TreeLongObjectRangeMap<>(); + + map.put(0L, 9L, null); + + assertThat(map.containsKey(5L)).isTrue(); + assertThat(map.get(5L)).isNull(); + assertThat(map.getOrDefault(5L, "fallback")).isNull(); + } + + @Test + void removesSingleKeysAndRanges() { + LongObjectRangeMap map = new TreeLongObjectRangeMap<>(); + map.put(0L, 99L, STORED_VALUE); + + map.remove(50L, 50L); + map.remove(20L, 29L); + + assertThat(map.get(19L)).isEqualTo(STORED_VALUE); + assertThat(map.containsKey(20L)).isFalse(); + assertThat(map.containsKey(29L)).isFalse(); + assertThat(map.get(30L)).isEqualTo(STORED_VALUE); + assertThat(map.containsKey(50L)).isFalse(); + assertThat(map.get(51L)).isEqualTo(STORED_VALUE); + } + + @Test + void supportsOpenClosedAndUnboundedRanges() { + LongObjectRangeMap map = new TreeLongObjectRangeMap<>(); + + map.put(10L, LongBoundType.OPEN, 20L, LongBoundType.CLOSED, "a"); + map.put(Long.MIN_VALUE, LongBoundType.CLOSED, 0L, LongBoundType.OPEN, "b"); + map.put(100L, LongBoundType.OPEN, Long.MAX_VALUE, LongBoundType.CLOSED, "c"); + + assertThat(map.containsKey(10L)).isFalse(); + assertThat(map.get(11L)).isEqualTo("a"); + assertThat(map.get(20L)).isEqualTo("a"); + assertThat(map.get(-1L)).isEqualTo("b"); + assertThat(map.containsKey(0L)).isFalse(); + assertThat(map.containsKey(100L)).isFalse(); + assertThat(map.get(101L)).isEqualTo("c"); + } + + @Test + void rejectsInvalidRangesAndNullBoundTypes() { + LongObjectRangeMap map = new TreeLongObjectRangeMap<>(); + + assertThatThrownBy(() -> map.put(2L, 1L, STORED_VALUE)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("fromInclusive must be <= toInclusive"); + assertThatThrownBy(() -> map.remove(2L, 1L)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("fromInclusive must be <= toInclusive"); + assertThatThrownBy(() -> map.put(1L, null, 2L, LongBoundType.CLOSED, STORED_VALUE)) + .isInstanceOf(NullPointerException.class) + .hasMessage("lowerType must not be null"); + assertThatThrownBy(() -> map.putCoalescing(1L, LongBoundType.CLOSED, 2L, null, STORED_VALUE)) + .isInstanceOf(NullPointerException.class) + .hasMessage("upperType must not be null"); + assertThatThrownBy(() -> map.remove(1L, LongBoundType.OPEN, 1L, LongBoundType.OPEN)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("open range endpoints must be different"); + } + + @Test + void matchesPointModelForRandomOperations() { + LongObjectRangeMap map = new TreeLongObjectRangeMap<>(); + Map expected = new HashMap<>(); + SplittableRandom random = new SplittableRandom(0x72616e67656d6170L); + + for (int i = 0; i < 10_000; i++) { + long from = random.nextLong(-50L, 51L); + long to = random.nextLong(from, 51L); + int operation = random.nextInt(3); + + if (operation == 0) { + map.remove(from, to); + for (long key = from; key <= to; key++) { + expected.remove(key); + } + } else { + String value = STORED_VALUE + "-" + random.nextLong(); + map.put(from, to, value); + for (long key = from; key <= to; key++) { + expected.put(key, value); + } + } + + for (long key = -50L; key <= 50L; key++) { + assertThat(map.containsKey(key)).isEqualTo(expected.containsKey(key)); + assertThat(map.getOrDefault(key, "missing")) + .isEqualTo(expected.getOrDefault(key, "missing")); + } + } + } + + private static String sameValueCopy() { + return SAME_VALUE.substring(0, 2) + SAME_VALUE.substring(2); + } +} diff --git a/mapsmith-core/src/test/java/name/mrkandreev/mapsmith/ranking/OrderStatisticLongLongMapTest.java b/mapsmith-core/src/test/java/name/mrkandreev/mapsmith/ranking/OrderStatisticLongLongMapTest.java new file mode 100644 index 0000000..8b585c1 --- /dev/null +++ b/mapsmith-core/src/test/java/name/mrkandreev/mapsmith/ranking/OrderStatisticLongLongMapTest.java @@ -0,0 +1,129 @@ +package name.mrkandreev.mapsmith.ranking; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.SplittableRandom; +import org.junit.jupiter.api.Test; + +class OrderStatisticLongLongMapTest { + @Test + void ranksEntriesByValueDescending() { + LongLongRankingMap map = new OrderStatisticLongLongMap(); + + map.put(10L, 100L); + map.put(20L, 300L); + map.put(30L, 200L); + + assertThat(map.rankOf(20L)).isEqualTo(1); + assertThat(map.rankOf(30L)).isEqualTo(2); + assertThat(map.rankOf(10L)).isEqualTo(3); + assertThat(map.countBefore(30L)).isEqualTo(1); + assertThat(map.countAfter(30L)).isEqualTo(1); + } + + @Test + void usesKeyAsTieBreakerForEqualValues() { + LongLongRankingMap map = new OrderStatisticLongLongMap(); + + map.put(30L, 100L); + map.put(10L, 100L); + map.put(20L, 100L); + + assertThat(map.rankOf(10L)).isEqualTo(1); + assertThat(map.rankOf(20L)).isEqualTo(2); + assertThat(map.rankOf(30L)).isEqualTo(3); + } + + @Test + void updatesRankWhenValueChanges() { + LongLongRankingMap map = new OrderStatisticLongLongMap(); + map.put(1L, 10L); + map.put(2L, 20L); + map.put(3L, 30L); + + assertThat(map.put(1L, 40L)).isEqualTo(10L); + + assertThat(map.rankOf(1L)).isEqualTo(1); + assertThat(map.rankOf(3L)).isEqualTo(2); + assertThat(map.rankOf(2L)).isEqualTo(3); + } + + @Test + void removesEntriesFromRanking() { + LongLongRankingMap map = new OrderStatisticLongLongMap(); + map.put(1L, 10L); + map.put(2L, 20L); + map.put(3L, 30L); + + assertThat(map.remove(3L)).isEqualTo(30L); + + assertThat(map.rankOf(2L)).isEqualTo(1); + assertThat(map.countBefore(1L)).isEqualTo(1); + assertThat(map.countAfter(1L)).isZero(); + } + + @Test + void returnsMissingRankForAbsentKeys() { + LongLongRankingMap map = new OrderStatisticLongLongMap(); + + assertThat(map.rankOf(404L)).isEqualTo(LongLongRankingMap.MISSING_RANK); + assertThat(map.countBefore(404L)).isEqualTo(LongLongRankingMap.MISSING_RANK); + assertThat(map.countAfter(404L)).isEqualTo(LongLongRankingMap.MISSING_RANK); + } + + @Test + void clearsRankedEntries() { + LongLongRankingMap map = new OrderStatisticLongLongMap(); + map.put(1L, 10L); + map.put(2L, 20L); + + map.clear(); + + assertThat(map.isEmpty()).isTrue(); + assertThat(map.rankOf(1L)).isEqualTo(LongLongRankingMap.MISSING_RANK); + } + + @Test + void matchesExpectedRanksForRandomOperations() { + LongLongRankingMap map = new OrderStatisticLongLongMap(1); + Map expected = new HashMap<>(); + SplittableRandom random = new SplittableRandom(0x6c656164657273L); + + for (int i = 0; i < 10_000; i++) { + long key = random.nextLong(500L); + long value = random.nextLong(1_000L); + int operation = random.nextInt(4); + + if (operation == 0) { + Long previousValue = expected.remove(key); + assertThat(map.remove(key)).isEqualTo(previousValue == null ? 0L : previousValue); + } else { + Long previousValue = expected.put(key, value); + assertThat(map.put(key, value)).isEqualTo(previousValue == null ? 0L : previousValue); + } + + assertThat(map.size()).isEqualTo(expected.size()); + assertRanks(map, expected); + } + } + + private static void assertRanks(LongLongRankingMap map, Map expected) { + List> ordered = new ArrayList<>(expected.entrySet()); + ordered.sort( + Comparator.>comparingLong(Map.Entry::getValue) + .reversed() + .thenComparingLong(Map.Entry::getKey)); + + for (int index = 0; index < ordered.size(); index++) { + long key = ordered.get(index).getKey(); + assertThat(map.rankOf(key)).isEqualTo(index + 1); + assertThat(map.countBefore(key)).isEqualTo(index); + assertThat(map.countAfter(key)).isEqualTo(ordered.size() - index - 1); + } + } +} diff --git a/mapsmith-samples/build.gradle.kts b/mapsmith-samples/build.gradle.kts new file mode 100644 index 0000000..3198226 --- /dev/null +++ b/mapsmith-samples/build.gradle.kts @@ -0,0 +1,5 @@ +plugins { application } + +dependencies { implementation(project(":mapsmith-core")) } + +application { mainClass = "name.mrkandreev.mapsmith.samples.Main" } diff --git a/mapsmith-samples/src/main/java/name/mrkandreev/mapsmith/samples/CustomOpenAddressStrategyExample.java b/mapsmith-samples/src/main/java/name/mrkandreev/mapsmith/samples/CustomOpenAddressStrategyExample.java new file mode 100644 index 0000000..a7e7eaa --- /dev/null +++ b/mapsmith-samples/src/main/java/name/mrkandreev/mapsmith/samples/CustomOpenAddressStrategyExample.java @@ -0,0 +1,21 @@ +package name.mrkandreev.mapsmith.samples; + +import name.mrkandreev.mapsmith.LongLongMap; +import name.mrkandreev.mapsmith.openaddressing.LongHashing; +import name.mrkandreev.mapsmith.openaddressing.LongLongOpenAddressMap; +import name.mrkandreev.mapsmith.openaddressing.LongLongOpenAddressingStrategy; + +public enum CustomOpenAddressStrategyExample { + ; + + public static void main(String[] args) { + LongLongOpenAddressingStrategy strategy = LongLongOpenAddressingStrategy.ROBIN_HOOD; + LongLongMap counters = new LongLongOpenAddressMap(strategy, 64, LongHashing.MURMUR3_FINALIZER); + + counters.put(42L, counters.get(42L) + 1L); + counters.put(42L, counters.get(42L) + 1L); + + System.out.println("Custom open-addressing strategy"); + System.out.printf("event 42 count = %d%n", counters.get(42L)); + } +} diff --git a/mapsmith-samples/src/main/java/name/mrkandreev/mapsmith/samples/Main.java b/mapsmith-samples/src/main/java/name/mrkandreev/mapsmith/samples/Main.java new file mode 100644 index 0000000..66d7f0a --- /dev/null +++ b/mapsmith-samples/src/main/java/name/mrkandreev/mapsmith/samples/Main.java @@ -0,0 +1,15 @@ +package name.mrkandreev.mapsmith.samples; + +public enum Main { + ; + + public static void main(String[] args) { + OpenAddressMapExample.main(args); + System.out.println(); + CustomOpenAddressStrategyExample.main(args); + System.out.println(); + RankingMapExample.main(args); + System.out.println(); + RangeMapExample.main(args); + } +} diff --git a/mapsmith-samples/src/main/java/name/mrkandreev/mapsmith/samples/OpenAddressMapExample.java b/mapsmith-samples/src/main/java/name/mrkandreev/mapsmith/samples/OpenAddressMapExample.java new file mode 100644 index 0000000..80c086a --- /dev/null +++ b/mapsmith-samples/src/main/java/name/mrkandreev/mapsmith/samples/OpenAddressMapExample.java @@ -0,0 +1,23 @@ +package name.mrkandreev.mapsmith.samples; + +import name.mrkandreev.mapsmith.LongLongMap; +import name.mrkandreev.mapsmith.openaddressing.LongHashing; +import name.mrkandreev.mapsmith.openaddressing.LongLongMapFactory; +import name.mrkandreev.mapsmith.openaddressing.MapSpecialization; + +public enum OpenAddressMapExample { + ; + + public static void main(String[] args) { + LongLongMap balances = + LongLongMapFactory.create(MapSpecialization.SWISS_TABLE, 1_000, LongHashing.FIBONACCI); + + balances.put(101L, 2_500L); + balances.put(102L, 7_000L); + balances.put(101L, 2_750L); + + System.out.println("Open address map"); + System.out.printf("user 101 balance = %d%n", balances.get(101L)); + System.out.printf("user 102 exists = %b%n", balances.containsKey(102L)); + } +} diff --git a/mapsmith-samples/src/main/java/name/mrkandreev/mapsmith/samples/RangeMapExample.java b/mapsmith-samples/src/main/java/name/mrkandreev/mapsmith/samples/RangeMapExample.java new file mode 100644 index 0000000..c259b0d --- /dev/null +++ b/mapsmith-samples/src/main/java/name/mrkandreev/mapsmith/samples/RangeMapExample.java @@ -0,0 +1,25 @@ +package name.mrkandreev.mapsmith.samples; + +import name.mrkandreev.mapsmith.range.LongBoundType; +import name.mrkandreev.mapsmith.range.LongLongRangeMap; +import name.mrkandreev.mapsmith.range.TreeLongLongRangeMap; + +public enum RangeMapExample { + ; + + public static void main(String[] args) { + LongLongRangeMap tiers = new TreeLongLongRangeMap(); + + tiers.put(0L, 999L, 1L); + tiers.put(1_000L, 4_999L, 2L); + tiers.put(5_000L, LongBoundType.CLOSED, 10_000L, LongBoundType.OPEN, 3L); + + System.out.println("Range map"); + System.out.printf("score 750 tier = %d%n", tiers.get(750L)); + System.out.printf("score 2500 tier = %d%n", tiers.get(2_500L)); + System.out.printf("score 10000 tier exists = %b%n", tiers.containsKey(10_000L)); + + tiers.remove(900L, 1_100L); + System.out.printf("score 950 tier after exclusion = %d%n", tiers.getOrDefault(950L, -1L)); + } +} diff --git a/mapsmith-samples/src/main/java/name/mrkandreev/mapsmith/samples/RankingMapExample.java b/mapsmith-samples/src/main/java/name/mrkandreev/mapsmith/samples/RankingMapExample.java new file mode 100644 index 0000000..5434f42 --- /dev/null +++ b/mapsmith-samples/src/main/java/name/mrkandreev/mapsmith/samples/RankingMapExample.java @@ -0,0 +1,23 @@ +package name.mrkandreev.mapsmith.samples; + +import name.mrkandreev.mapsmith.ranking.LongLongRankingMap; +import name.mrkandreev.mapsmith.ranking.OrderStatisticLongLongMap; + +public enum RankingMapExample { + ; + + public static void main(String[] args) { + LongLongRankingMap leaderboard = new OrderStatisticLongLongMap(); + + leaderboard.put(10L, 1_200L); + leaderboard.put(20L, 3_400L); + leaderboard.put(30L, 2_100L); + leaderboard.put(40L, 3_400L); + + System.out.println("Ranking map"); + System.out.printf("user 20 rank = %d%n", leaderboard.rankOf(20L)); + System.out.printf("user 30 rank = %d%n", leaderboard.rankOf(30L)); + System.out.printf("entries before user 30 = %d%n", leaderboard.countBefore(30L)); + System.out.printf("entries after user 30 = %d%n", leaderboard.countAfter(30L)); + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..7c0a507 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,7 @@ +rootProject.name = "mapsmith" + +include( + "mapsmith-core", + "mapsmith-benchmarks", + "mapsmith-samples", +)