Add thread-safe RiaStringPool utility and unit tests

Introduce RiaStringPool for efficient shared string storage and retrieval by index, with thread-safe access using shared mutex. Pre-allocate empty string at index 0. Add comprehensive unit tests covering core functionality, concurrency, edge cases, and realistic usage.
This commit is contained in:
Magne Sjaastad committed 2026-02-05 13:49:00 +01:00
1 parent 2573c17aed
commit cff1ab08c4
5 files changed
+496

No files matched your search

@@ -23,6 +23,7 @@ set(SOURCE_GROUP_HEADER_FILES
${CMAKE_CURRENT_LIST_DIR}/RiaEclipseFileNameTools.h
${CMAKE_CURRENT_LIST_DIR}/RiaFeatureCommandContext.h
${CMAKE_CURRENT_LIST_DIR}/RiaStringListSerializer.h
${CMAKE_CURRENT_LIST_DIR}/RiaStringPool.h
${CMAKE_CURRENT_LIST_DIR}/RiaNncDefines.h
${CMAKE_CURRENT_LIST_DIR}/RiaPlotDefines.h
${CMAKE_CURRENT_LIST_DIR}/RiaStimPlanModelDefines.h
@@ -67,6 +68,7 @@ set(SOURCE_GROUP_SOURCE_FILES
${CMAKE_CURRENT_LIST_DIR}/RiaEclipseFileNameTools.cpp
${CMAKE_CURRENT_LIST_DIR}/RiaFeatureCommandContext.cpp
${CMAKE_CURRENT_LIST_DIR}/RiaStringListSerializer.cpp
${CMAKE_CURRENT_LIST_DIR}/RiaStringPool.cpp
${CMAKE_CURRENT_LIST_DIR}/RiaNncDefines.cpp
${CMAKE_CURRENT_LIST_DIR}/RiaPlotDefines.cpp
${CMAKE_CURRENT_LIST_DIR}/RiaStimPlanModelDefines.cpp
@@ -0,0 +1,87 @@
/////////////////////////////////////////////////////////////////////////////////
//
// Copyright (C) 2026 Equinor ASA
//
// ResInsight is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE.
//
// See the GNU General Public License at <http://www.gnu.org/licenses/gpl.html>
// for more details.
//
/////////////////////////////////////////////////////////////////////////////////
#include "RiaStringPool.h"
#include <mutex>
#include <stdexcept>
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
RiaStringPool& RiaStringPool::instance()
{
static RiaStringPool pool;
return pool;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
RiaStringPool::RiaStringPool()
{
// Pre-allocate empty string at index 0
m_strings.push_back( "" );
m_stringToIndex[""] = 0;
m_emptyIndex = 0;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
RiaStringPool::IndexType RiaStringPool::getIndex( const std::string& str )
{
// First try with shared lock for reading
{
std::shared_lock<std::shared_mutex> lock( m_mutex );
auto it = m_stringToIndex.find( str );
if ( it != m_stringToIndex.end() )
{
return it->second;
}
}
// Need to insert, acquire exclusive lock
std::unique_lock<std::shared_mutex> lock( m_mutex );
// Double-check after acquiring exclusive lock (another thread might have inserted it)
auto it = m_stringToIndex.find( str );
if ( it != m_stringToIndex.end() )
{
return it->second;
}
IndexType newIndex = static_cast<IndexType>( m_strings.size() );
m_strings.push_back( str );
m_stringToIndex[m_strings.back()] = newIndex;
return newIndex;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
const std::string& RiaStringPool::getString( IndexType index ) const
{
std::shared_lock<std::shared_mutex> lock( m_mutex );
if ( index >= m_strings.size() )
{
throw std::out_of_range( "String pool index out of range" );
}
return m_strings[index];
}
@@ -0,0 +1,57 @@
/////////////////////////////////////////////////////////////////////////////////
//
// Copyright (C) 2026 Equinor ASA
//
// ResInsight is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE.
//
// See the GNU General Public License at <http://www.gnu.org/licenses/gpl.html>
// for more details.
//
/////////////////////////////////////////////////////////////////////////////////
#pragma once
#include <cstdint>
#include <shared_mutex>
#include <string>
#include <unordered_map>
#include <vector>
//==================================================================================================
/// String pool for memory-efficient storage of shared strings
/// Stores strings once and returns indices for lookups
//==================================================================================================
class RiaStringPool
{
public:
using IndexType = uint32_t;
static constexpr IndexType INVALID_INDEX = static_cast<IndexType>( -1 );
static RiaStringPool& instance();
// Get or create an index for a string
IndexType getIndex( const std::string& str );
// Get string by index
const std::string& getString( IndexType index ) const;
// Get empty string index
IndexType getEmptyIndex() const { return m_emptyIndex; }
private:
RiaStringPool();
RiaStringPool( const RiaStringPool& ) = delete;
RiaStringPool& operator=( const RiaStringPool& ) = delete;
mutable std::shared_mutex m_mutex;
std::vector<std::string> m_strings;
std::unordered_map<std::string, IndexType> m_stringToIndex;
IndexType m_emptyIndex;
};
@@ -69,6 +69,7 @@ set(SOURCE_UNITTEST_FILES
${CMAKE_CURRENT_LIST_DIR}/RifCsvDataTableFormatter-Test.cpp
${CMAKE_CURRENT_LIST_DIR}/RiaSummaryAddressAnalyzer-Test.cpp
${CMAKE_CURRENT_LIST_DIR}/RiaStdStringTools-Test.cpp
${CMAKE_CURRENT_LIST_DIR}/RiaStringPool-Test.cpp
${CMAKE_CURRENT_LIST_DIR}/RiaInterpolationTools-Test.cpp
${CMAKE_CURRENT_LIST_DIR}/RifWellMeasurementReader-Test.cpp
${CMAKE_CURRENT_LIST_DIR}/RiaDateStringParser-Test.cpp
@@ -0,0 +1,349 @@
#include "gtest/gtest.h"
#include "RiaStringPool.h"
#include <algorithm>
#include <set>
#include <thread>
#include <vector>
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
TEST( RiaStringPoolTest, EmptyStringPreAllocated )
{
auto& pool = RiaStringPool::instance();
auto emptyIndex = pool.getIndex( "" );
EXPECT_EQ( pool.getEmptyIndex(), emptyIndex );
EXPECT_EQ( "", pool.getString( emptyIndex ) );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
TEST( RiaStringPoolTest, BasicStringStorage )
{
auto& pool = RiaStringPool::instance();
std::string testStr = "FOPT";
auto index = pool.getIndex( testStr );
EXPECT_EQ( testStr, pool.getString( index ) );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
TEST( RiaStringPoolTest, DuplicateStringsReturnSameIndex )
{
auto& pool = RiaStringPool::instance();
std::string testStr = "WOPT:WELL1";
auto index1 = pool.getIndex( testStr );
auto index2 = pool.getIndex( testStr );
auto index3 = pool.getIndex( testStr );
EXPECT_EQ( index1, index2 );
EXPECT_EQ( index2, index3 );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
TEST( RiaStringPoolTest, MultipleUniqueStrings )
{
auto& pool = RiaStringPool::instance();
std::vector<std::string> testStrings = { "FOPT", "WOPT:WELL1", "GOPT:GROUP1", "BPR:10,20,30", "ROPR" };
std::vector<RiaStringPool::IndexType> indices;
for ( const auto& str : testStrings )
{
indices.push_back( pool.getIndex( str ) );
}
// All indices should be unique
std::set<RiaStringPool::IndexType> uniqueIndices( indices.begin(), indices.end() );
EXPECT_EQ( testStrings.size(), uniqueIndices.size() );
// Verify retrieval
for ( size_t i = 0; i < testStrings.size(); ++i )
{
EXPECT_EQ( testStrings[i], pool.getString( indices[i] ) );
}
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
TEST( RiaStringPoolTest, OutOfRangeThrows )
{
auto& pool = RiaStringPool::instance();
// Attempt to access index far beyond current size
EXPECT_THROW( pool.getString( 9999999 ), std::out_of_range );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
TEST( RiaStringPoolTest, ThreadSafety )
{
auto& pool = RiaStringPool::instance();
const int numThreads = 10;
const int stringsPerThread = 100;
std::vector<std::thread> threads;
std::vector<std::vector<RiaStringPool::IndexType>> threadResults( numThreads );
// Create threads that all add the same strings
for ( int t = 0; t < numThreads; ++t )
{
threads.emplace_back(
[&pool, &threadResults, t, stringsPerThread]()
{
for ( int i = 0; i < stringsPerThread; ++i )
{
std::string str = "STRING_" + std::to_string( i );
auto index = pool.getIndex( str );
threadResults[t].push_back( index );
}
} );
}
// Wait for all threads to complete
for ( auto& thread : threads )
{
thread.join();
}
// Verify all threads got the same indices for the same strings
for ( int i = 1; i < numThreads; ++i )
{
EXPECT_EQ( threadResults[0].size(), threadResults[i].size() );
for ( size_t j = 0; j < threadResults[0].size(); ++j )
{
EXPECT_EQ( threadResults[0][j], threadResults[i][j] );
}
}
// Verify string retrieval
for ( int i = 0; i < stringsPerThread; ++i )
{
std::string expectedStr = "STRING_" + std::to_string( i );
auto index = threadResults[0][i];
EXPECT_EQ( expectedStr, pool.getString( index ) );
}
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
TEST( RiaStringPoolTest, LargeNumberOfStrings )
{
auto& pool = RiaStringPool::instance();
const int numStrings = 10000;
std::vector<RiaStringPool::IndexType> indices;
// Add many unique strings
for ( int i = 0; i < numStrings; ++i )
{
std::string str = "VECTOR_" + std::to_string( i ) + "_DATA";
indices.push_back( pool.getIndex( str ) );
}
// Verify all can be retrieved
for ( int i = 0; i < numStrings; ++i )
{
std::string expectedStr = "VECTOR_" + std::to_string( i ) + "_DATA";
EXPECT_EQ( expectedStr, pool.getString( indices[i] ) );
}
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
TEST( RiaStringPoolTest, SpecialCharacters )
{
auto& pool = RiaStringPool::instance();
std::vector<std::string> specialStrings = { "String with spaces",
"String:with:colons",
"String/with/slashes",
"String,with,commas",
"String-with-dashes",
"String_with_underscores",
"String.with.dots",
"String|with|pipes",
"String@with@at",
"String#with#hash" };
for ( const auto& str : specialStrings )
{
auto index = pool.getIndex( str );
EXPECT_EQ( str, pool.getString( index ) );
}
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
TEST( RiaStringPoolTest, VeryLongStrings )
{
auto& pool = RiaStringPool::instance();
// Create a very long string
std::string longStr( 10000, 'A' );
longStr += "_UNIQUE";
auto index = pool.getIndex( longStr );
EXPECT_EQ( longStr, pool.getString( index ) );
// Verify same index on duplicate
auto index2 = pool.getIndex( longStr );
EXPECT_EQ( index, index2 );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
TEST( RiaStringPoolTest, ConcurrentInsertAndRead )
{
auto& pool = RiaStringPool::instance();
const int numWriteThreads = 5;
const int numReadThreads = 5;
const int stringsPerWrite = 50;
std::vector<std::thread> threads;
// Create strings in multiple write threads
for ( int t = 0; t < numWriteThreads; ++t )
{
threads.emplace_back(
[&pool, t, stringsPerWrite]()
{
for ( int i = 0; i < stringsPerWrite; ++i )
{
std::string str = "WRITE_" + std::to_string( t ) + "_" + std::to_string( i );
pool.getIndex( str );
}
} );
}
// Read strings concurrently
for ( int t = 0; t < numReadThreads; ++t )
{
threads.emplace_back(
[&pool, t, stringsPerWrite]()
{
for ( int i = 0; i < stringsPerWrite; ++i )
{
// Try to read strings that might be being created
std::string str = "WRITE_0_" + std::to_string( i );
auto index = pool.getIndex( str );
// Should not crash even if racing with writes
EXPECT_EQ( str, pool.getString( index ) );
}
} );
}
for ( auto& thread : threads )
{
thread.join();
}
// Verify all written strings are accessible
for ( int t = 0; t < numWriteThreads; ++t )
{
for ( int i = 0; i < stringsPerWrite; ++i )
{
std::string str = "WRITE_" + std::to_string( t ) + "_" + std::to_string( i );
auto index = pool.getIndex( str );
EXPECT_EQ( str, pool.getString( index ) );
}
}
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
TEST( RiaStringPoolTest, InvalidIndex )
{
auto& pool = RiaStringPool::instance();
// Test with INVALID_INDEX constant
EXPECT_THROW( pool.getString( RiaStringPool::INVALID_INDEX ), std::out_of_range );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
TEST( RiaStringPoolTest, SimilarStrings )
{
auto& pool = RiaStringPool::instance();
// Test strings that are similar but not identical
std::vector<std::string> similarStrings = { "WOPT:WELL-1",
"WOPT:WELL-2",
"WOPT:WELL-10",
"WOPT:WELL-1A",
"WOPT:WELL_1",
"WOPT: WELL-1" };
std::vector<RiaStringPool::IndexType> indices;
for ( const auto& str : similarStrings )
{
indices.push_back( pool.getIndex( str ) );
}
// All should have different indices
std::set<RiaStringPool::IndexType> uniqueIndices( indices.begin(), indices.end() );
EXPECT_EQ( similarStrings.size(), uniqueIndices.size() );
// Verify each string retrieval
for ( size_t i = 0; i < similarStrings.size(); ++i )
{
EXPECT_EQ( similarStrings[i], pool.getString( indices[i] ) );
}
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
TEST( RiaStringPoolTest, CommonSummaryVectorNames )
{
auto& pool = RiaStringPool::instance();
// Test with realistic summary vector names
std::vector<std::string> vectorNames = { "FOPT",
"FWPT",
"FGPT",
"FOPR",
"FWPR",
"FGPR",
"WOPT:PROD-1",
"WWPT:PROD-1",
"WBHP:PROD-1",
"WOPT:INJ-1",
"GOPT:FIELD",
"BPR:10,15,20" };
// Store indices
std::vector<RiaStringPool::IndexType> indices;
for ( const auto& name : vectorNames )
{
indices.push_back( pool.getIndex( name ) );
}
// Request same strings again - should get same indices
for ( size_t i = 0; i < vectorNames.size(); ++i )
{
auto index = pool.getIndex( vectorNames[i] );
EXPECT_EQ( indices[i], index );
EXPECT_EQ( vectorNames[i], pool.getString( index ) );
}
}