Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
61255d6
Fix FlatMap copy assignment -- need to compare addresses, not values
kennyweiss Jun 10, 2026
2baf6a5
Remove FlatMap's const operator[], which inserts for missing keys
kennyweiss Jun 10, 2026
d1cb266
DeviceHash: hash in 64 bits regardless of IndexType width
kennyweiss Jun 10, 2026
51f9a86
DeviceHash: hash floating-point keys by bit pattern, not value
kennyweiss Jun 10, 2026
2c00602
FlatTable: wrap probe advance with a mask, not a signed division
kennyweiss Jun 10, 2026
c6555a8
Adds initial benchmark for flatmap vs map vs unordered_map vs sparsehash
kennyweiss Jan 16, 2026
09dc808
Improves performance of FlatMap batched insertion for SEQ policy
kennyweiss Jan 16, 2026
acdf683
Adds FlatMap benchmarks for hits and misses of precached entities
kennyweiss Jan 16, 2026
862195a
Exploring faster hash functions
kennyweiss Jan 16, 2026
296934c
Adds benchmark for flatmap load factor
kennyweiss Mar 10, 2026
8b6cb58
Benchmark: decouple lookup order from insertion order in FlatMap suite
kennyweiss Jun 11, 2026
324c90a
FlatMap: force-inline the lookup hot path
kennyweiss Jun 11, 2026
03ac4d9
FlatMap: hash once and avoid FP division in getEmplacePos
kennyweiss Jun 11, 2026
f658482
Benchmark: report realized load factor in target-load-factor scenarios
kennyweiss Jun 11, 2026
1935fe6
FlatTable: honor visitor early-exit in scalar visitHashBucket
kennyweiss Jun 11, 2026
cb5793a
Fixes hip build via missing AXOM_HOST_DEVICE
kennyweiss Jun 11, 2026
6691dbb
FlatMap: Fuse the find and empty-slot probes in getEmplacePos()
kennyweiss Jun 11, 2026
6d8a86f
FlatMap: Keep move semantics during batch insertion
kennyweiss Jun 11, 2026
f083e50
Improves FlatMap benchmark
kennyweiss Jun 11, 2026
e10cd02
FlatMap: Device hash type must be 64 bits
kennyweiss Jun 11, 2026
e95e7df
Moves AXOM_FORCE_INLINE to core's Macros.hpp
kennyweiss Jun 11, 2026
4f34f61
Adds utility function for initializing initial probe group via bitshi…
kennyweiss Jun 11, 2026
d409ece
FlatMap: Improves documentation and testing of find_with_hash
kennyweiss Jun 11, 2026
9b14075
Adds benchmarks for device contruction and lookup
kennyweiss Jun 11, 2026
489d9ff
FlatMap: Generalizes the device benchmarks to other execution spaces,…
kennyweiss Jun 11, 2026
fd31c5e
Add number of threads to omp benchmarks
kennyweiss Jun 12, 2026
a85db82
Updates RELEASE-NOTES
kennyweiss Jun 12, 2026
d8bb8e9
Bugfix for rzvector -- `if constexpr` needs an `else`
kennyweiss Jun 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions RELEASE-NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ The Axom project release numbers follow [Semantic Versioning](http://semver.org/
- Primal: Improves reproducibility of 3D GWN methods by removing some sources of randomness
- Core: ArrayView assigments/copies now copy the stride
- Core: Array construction from strided ArrayView now correctly copies the strided elements
- Core: Improved `axom::FlatMap` insertion performance by fusing duplicate-key lookup with empty-slot probing.
- Core: Updated DeviceHash to use 64-bit hash results and improved coverage for integer and floating-point hashing.

## [Version 0.14.0] - Release date 2026-03-31

Expand Down
2 changes: 1 addition & 1 deletion src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ else()
endif()
endif()

if (${PROJECT_SOURCE_DIR} STREQUAL ${CMAKE_SOURCE_DIR})
if ("${PROJECT_SOURCE_DIR}" STREQUAL "${CMAKE_SOURCE_DIR}")
# Set some default BLT options before loading BLT only if not included in
# another project
if (NOT BLT_CXX_STD)
Expand Down
59 changes: 43 additions & 16 deletions src/axom/core/DeviceHash.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
#include "axom/core/Macros.hpp"
#include "axom/core/Types.hpp"

#include <cstdint>
#include <cstring>
#include <type_traits>

namespace axom
Expand All @@ -25,24 +27,45 @@ template <typename T>
struct DeviceHashHelper<T, std::enable_if_t<std::is_integral<T>::value>>
{
using argument_type = T;
using result_type = axom::IndexType;
AXOM_HOST_DEVICE axom::IndexType operator()(T value) const { return value; }
using result_type = std::uint64_t;
AXOM_HOST_DEVICE std::uint64_t operator()(T value) const
{
return static_cast<std::uint64_t>(value);
}
};

/// \brief Specialization for floating-point types
template <typename T>
struct DeviceHashHelper<T, std::enable_if_t<std::is_floating_point<T>::value>>
{
using argument_type = T;
using result_type = axom::IndexType;
AXOM_HOST_DEVICE axom::IndexType operator()(T value) const
using result_type = std::uint64_t;
AXOM_HOST_DEVICE std::uint64_t operator()(T value) const
{
// Special case: -0.0 and 0.0 compare equal but have different byte representations.
// -0.0 and 0.0 compare equal but have different bit patterns; normalize so both hash identically
if(value == T {0.})
{
return 0;
value = T {0.};
}

// Hash the bit pattern, not the converted value.
// A float-to-integer value conversion collapses every key sharing an integer part,
// e.g. all numbers between -1 and 1 converts to integer 0

if constexpr(sizeof(T) <= sizeof(std::uint64_t))
{
// Zero-initialize first since we only copy 4 bytes for floats.
std::uint64_t result = 0;
memcpy(&result, &value, sizeof(T));
return result;
}
return value;

// Avoid hashing padding bytes for wider floating types such as x86 long double.
// Collisions are acceptable for a hash; equal values must hash identically.
double narrowed_value = static_cast<double>(value);
std::uint64_t result = 0;
memcpy(&result, &narrowed_value, sizeof(narrowed_value));
return result;
}
};

Expand All @@ -51,10 +74,10 @@ template <typename T>
struct DeviceHashHelper<T, std::enable_if_t<std::is_enum<T>::value>>
{
using argument_type = T;
using result_type = axom::IndexType;
AXOM_HOST_DEVICE axom::IndexType operator()(T value) const
using result_type = std::uint64_t;
AXOM_HOST_DEVICE std::uint64_t operator()(T value) const
{
return static_cast<axom::IndexType>(value);
return static_cast<std::uint64_t>(value);
}
};

Expand All @@ -63,10 +86,10 @@ template <typename T>
struct DeviceHashHelper<T*, void>
{
using argument_type = T*;
using result_type = axom::IndexType;
AXOM_HOST_DEVICE axom::IndexType operator()(T* ptr) const
using result_type = std::uint64_t;
AXOM_HOST_DEVICE std::uint64_t operator()(T* ptr) const
{
return static_cast<axom::IndexType>(reinterpret_cast<std::uintptr_t>(ptr));
return static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(ptr));
}
};

Expand All @@ -75,10 +98,10 @@ template <typename T, typename Enable>
struct DeviceHashHelper
{
using argument_type = T;
using result_type = axom::IndexType;
axom::IndexType operator()(const T& object) const
using result_type = std::uint64_t;
std::uint64_t operator()(const T& object) const
{
return static_cast<axom::IndexType>(std::hash<T> {}(object));
return static_cast<std::uint64_t>(std::hash<T> {}(object));
}
};

Expand All @@ -89,6 +112,10 @@ struct DeviceHashHelper
*
* \brief Implements a host/device-callable hash function for supported types,
* and passes through to std::hash otherwise.
*
* The result type is always std::uint64_t, independent of the configured axom::IndexType width.
* Hashes feed bit mixers and bucket selection, where truncating wide keys (e.g. 64-bit Morton codes)
* to a 32-bit IndexType before mixing would make keys equal mod 2^32 collide.
*/
template <typename T>
struct DeviceHash : public detail::DeviceHashHelper<T>
Expand Down
78 changes: 58 additions & 20 deletions src/axom/core/FlatMap.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#ifndef Axom_Core_FlatMap_HPP
#define Axom_Core_FlatMap_HPP

#include <cstdint>
#include <tuple>
#include <type_traits>
#include <utility>
Expand Down Expand Up @@ -74,6 +75,8 @@ class FlatMap : detail::flat_map::SequentialLookupPolicy<typename Hash::result_t
using mapped_type = ValueType;
using size_type = IndexType;
using value_type = KeyValuePair;
using hasher = Hash;
using hash_result_type = typename Hash::result_type;
using iterator = IteratorImpl<false>;
using const_iterator = IteratorImpl<true>;

Expand Down Expand Up @@ -179,7 +182,7 @@ class FlatMap : detail::flat_map::SequentialLookupPolicy<typename Hash::result_t
static_assert(std::is_copy_constructible<ValueType>::value,
"Cannot copy an axom::FlatMap when value type is not "
"copy-constructible.");
if(*this != other)
if(this != &other)
{
FlatMap new_map(other);
swap(new_map);
Expand Down Expand Up @@ -287,8 +290,25 @@ class FlatMap : detail::flat_map::SequentialLookupPolicy<typename Hash::result_t
* if the key wasn't found.
*/
/// @{
iterator find(const KeyType& key);
const_iterator find(const KeyType& key) const;
AXOM_FORCE_INLINE iterator find(const KeyType& key);
AXOM_FORCE_INLINE const_iterator find(const KeyType& key) const;
/// @}

/*!
* \brief Try to find an entry with a given key and a precomputed hash.
*
* \param [in] key the key to search for
* \param [in] hash the precomputed hash for \a key
*
* \return An iterator pointing to the corresponding key-value pair, or end()
* if the key wasn't found.
*
* \pre hash must be equivalent to hasher{}(key) for this FlatMap's Hash policy.
* Supplying a hash computed for a different key or Hash policy can miss an existing key
*/
/// @{
AXOM_FORCE_INLINE iterator find_with_hash(const KeyType& key, hash_result_type hash);
AXOM_FORCE_INLINE const_iterator find_with_hash(const KeyType& key, hash_result_type hash) const;
/// @}

/*!
Expand Down Expand Up @@ -332,22 +352,13 @@ class FlatMap : detail::flat_map::SequentialLookupPolicy<typename Hash::result_t
*
* \pre ValueType is default-constructible
*/
/// @{
ValueType& operator[](const KeyType& key)
{
static_assert(std::is_default_constructible<ValueType>::value,
"Cannot use axom::FlatMap::operator[] when value type is not "
"default-constructible.");
return this->try_emplace(key).first->second;
}
const ValueType& operator[](const KeyType& key) const
{
static_assert(std::is_default_constructible<ValueType>::value,
"Cannot use axom::FlatMap::operator[] when value type is not "
"default-constructible.");
return this->try_emplace(key).first->second;
}
/// @}

/*!
* \brief Return the number of entries matching a given key.
Expand Down Expand Up @@ -840,7 +851,13 @@ FlatMap<KeyType, ValueType, Hash>::FlatMap(IndexType num_elems,
template <typename KeyType, typename ValueType, typename Hash>
auto FlatMap<KeyType, ValueType, Hash>::find(const KeyType& key) -> iterator
{
auto hash = Hash {}(key);
return find_with_hash(key, Hash {}(key));
}

template <typename KeyType, typename ValueType, typename Hash>
auto FlatMap<KeyType, ValueType, Hash>::find_with_hash(const KeyType& key, hash_result_type hash)
-> iterator
{
iterator found_iter = end();
this->probeIndex(m_numGroups2, m_metadata, hash, [&](IndexType bucket_index) -> bool {
if(this->m_buckets[bucket_index].get().first == key)
Expand All @@ -857,7 +874,13 @@ auto FlatMap<KeyType, ValueType, Hash>::find(const KeyType& key) -> iterator
template <typename KeyType, typename ValueType, typename Hash>
auto FlatMap<KeyType, ValueType, Hash>::find(const KeyType& key) const -> const_iterator
{
auto hash = Hash {}(key);
return find_with_hash(key, Hash {}(key));
}

template <typename KeyType, typename ValueType, typename Hash>
auto FlatMap<KeyType, ValueType, Hash>::find_with_hash(const KeyType& key, hash_result_type hash) const
-> const_iterator
{
const_iterator found_iter = end();
this->probeIndex(m_numGroups2, m_metadata, hash, [&](IndexType bucket_index) -> bool {
if(this->m_buckets[bucket_index].get().first == key)
Expand Down Expand Up @@ -888,23 +911,38 @@ auto FlatMap<KeyType, ValueType, Hash>::getEmplacePos(const KeyType& key)
{
auto hash = Hash {}(key);

// If the key already exists, return the existing iterator.
iterator existing_elem = this->find(key);
// Single fused probe: visit key matches and locate the insertion slot in a single pass
iterator existing_elem = this->end();
IndexType newBucket =
this->probeEmplaceIndex(m_numGroups2, m_metadata, hash, [&](IndexType bucket_index) -> bool {
if(this->m_buckets[bucket_index].get().first == key)
{
existing_elem = iterator(this, bucket_index);
return false;
}
return true;
});

if(existing_elem != this->end())
{
return {existing_elem, false};
}
// Resize to double the number of bucket groups if insertion would put us
// above the maximum load factor.
if(((m_loadCount + 1) / (double)bucket_count()) >= MAX_LOAD_FACTOR)
// MAX_LOAD_FACTOR is exactly 7/8, so (count + 1) / buckets >= 7/8 is
// equivalent to 8 * (count + 1) >= 7 * buckets in exact integer arithmetic.
// This avoids a floating-point division on every insertion.
static_assert(MAX_LOAD_FACTOR == 0.875,
"Integer load-factor check below assumes MAX_LOAD_FACTOR == 7/8.");
if(8 * (static_cast<std::uint64_t>(m_loadCount) + 1) >=
7 * static_cast<std::uint64_t>(bucket_count()))
{
IndexType newNumGroups = m_metadata.size() * 2;
rehash(newNumGroups * BucketsPerGroup - 1);
// The table was rebuilt, so the slot is stale. If we got here, the key is missing
newBucket = this->probeEmptyIndex(m_numGroups2, m_metadata, hash);
}

// Get an empty index to place the element into.
IndexType newBucket = this->probeEmptyIndex(m_numGroups2, m_metadata, hash);

// Add a hash to the corresponding bucket slot.
this->setBucketHash(m_metadata, newBucket, hash);
m_size++;
Expand Down
Loading