mirror of
https://github.com/OPM/ResInsight.git
synced 2026-10-24 22:27:55 -05:00
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:
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 ) );
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user