/// Cuts a bit sequence from fixed-size bit-string
/**
- The splitter an be used as iterator over bit-string.
+ The splitter can be used as iterator over bit-string.
Each call of \p cut() or \p safe_cut() cuts the bit count specified
and keeps the position inside bit-string for the next call.
//@endcond
public:
- /// Initializises the splitter with reference to \p h
+ /// Initializises the splitter with reference to \p h and zero start bit offset
explicit split_bitstring( bitstring const& h )
: m_ptr(reinterpret_cast<uint_type const*>( &h ))
, m_pos(0)
# endif
{}
+ /// Initializises the splitter with reference to \p h and start bit offset \p nBitOffset
+ split_bitstring( bitstring const& h, size_t nBitOffset )
+ : m_ptr( reinterpret_cast<uint_type const*>( &h ) + nBitOffset / c_nBitPerInt )
+ , m_pos( nBitOffset % c_nBitPerInt )
+ , m_first( m_ptr )
+# ifdef _DEBUG
+ , m_last( m_ptr + c_nHashSize )
+# endif
+ {}
+
+
/// Returns \p true if end-of-string is not reached yet
explicit operator bool() const
{
/// Auto hp_guard.
/**
- This class encapsulates Hazard Pointer guard to protect a pointer against deletion .
+ This class encapsulates Hazard Pointer guard to protect a pointer against deletion.
It allocates one HP from thread's HP array in constructor and free the hazard pointer allocated
in destructor.
*/
/// Creates empty guarded pointer
guarded_ptr() CDS_NOEXCEPT
: m_pGuard(nullptr)
- {}
+ {
+ alloc_guard();
+ }
//@cond
/// Initializes guarded pointer with \p p
explicit guarded_ptr( guarded_type * p ) CDS_NOEXCEPT
+ : m_pGuard( nullptr )
{
- alloc_guard();
- assert( m_pGuard );
- m_pGuard->set(p);
+ reset(p);
}
explicit guarded_ptr( std::nullptr_t ) CDS_NOEXCEPT
: m_pGuard( nullptr )
assert( m_pGuard );
return *m_pGuard;
}
+
+ void reset(guarded_type * p) CDS_NOEXCEPT
+ {
+ alloc_guard();
+ assert( m_pGuard );
+ m_pGuard->set(p);
+ }
//@endcond
private:
event_counter m_nSlotChanged; ///< Number of array node slot changing by other thread during an operation
event_counter m_nSlotConverting; ///< Number of events when we encounter a slot while it is converting to array node
+ event_counter m_nArrayNodeCount; ///< Number of array nodes
+
//@cond
void onInsertSuccess() { ++m_nInsertSuccess; }
- void onInserFailed() { ++m_nInsertFailed; }
+ void onInsertFailed() { ++m_nInsertFailed; }
void onInsertRetry() { ++m_nInsertRetry; }
void onUpdateNew() { ++m_nUpdateNew; }
void onUpdateExisting() { ++m_nUpdateExisting; }
void onExpandNodeFailed() { ++m_nExpandNodeFailed; }
void onSlotChanged() { ++m_nSlotChanged; }
void onSlotConverting() { ++m_nSlotConverting; }
+ void onArrayNodeCreated() { ++m_nArrayNodeCount; }
//@endcond
};
void onExpandNodeFailed() const {}
void onSlotChanged() const {}
void onSlotConverting() const {}
+ void onArrayNodeCreated() const {}
//@endcond
};
public:
/// Creates empty set
/**
- @param head_bits: 2<sup>head_bits</sup> specifies the size of head array, minimum is 8.
+ @param head_bits: 2<sup>head_bits</sup> specifies the size of head array, minimum is 4.
@param array_bits: 2<sup>array_bits</sup> specifies the size of array node, minimum is 2.
Equation for \p head_bits and \p array_bits:
typename gc::Guard guard;
back_off bkoff;
+ size_t nOffset = m_Metrics.head_node_size_log;
atomic_node_ptr * pArr = m_Head;
size_t nSlot = splitter.cut( m_Metrics.head_node_size_log );
assert( nSlot < m_Metrics.head_node_size );
nSlot = splitter.cut( m_Metrics.array_node_size_log );
assert( nSlot < m_Metrics.array_node_size );
pArr = to_array( slot.ptr() );
+ nOffset += m_Metrics.array_node_size_log;
}
else if ( slot.bits() == array_converting ) {
// the slot is converting to array node right now
}
// the slot must be expanded
- atomic_node_ptr * pNewArr;
- size_t nNewSlot;
- std::tie( pNewArr, nNewSlot ) = expand_slot( pArr[ nSlot ], slot, splitter );
- if ( pNewArr ) {
- pArr = pNewArr;
- nSlot = nNewSlot;
- }
+ expand_slot( pArr[ nSlot ], slot, nOffset );
}
else {
// the slot is empty, try to insert data node
(i.e. the item has been inserted or updated),
\p second is \p true if new item has been added or \p false if the set contains that hash.
*/
- template <typename Func>
std::pair<bool, bool> update( value_type& val, bool bInsert = true )
{
hash_type const& hash = hash_accessor()( val );
atomic_node_ptr * pArr = m_Head;
size_t nSlot = splitter.cut( m_Metrics.head_node_size_log );
assert( nSlot < m_Metrics.head_node_size );
+ size_t nOffset = m_Metrics.head_node_size_log;
while ( true ) {
node_ptr slot = pArr[nSlot].load( memory_model::memory_order_acquire );
nSlot = splitter.cut( m_Metrics.array_node_size_log );
assert( nSlot < m_Metrics.array_node_size );
pArr = to_array( slot.ptr() );
+ nOffset += m_Metrics.array_node_size_log;
}
else if ( slot.bits() == array_converting ) {
// the slot is converting to array node right now
if ( cmp( hash, hash_accessor()( *slot.ptr() )) == 0 ) {
// the item with that hash value already exists
// Replace it with val
- if ( pArr[nSlot].compare_exchange_strong( slot, node_ptr( &val ), memory_model::memory_order_release, atomics::memory_order_relaxed ) ) {
+ if ( slot.ptr() == &val ) {
+ m_Stat.onUpdateExisting();
+ return std::make_pair( true, false );
+ }
+
+ if ( pArr[nSlot].compare_exchange_strong( slot, node_ptr( &val ), memory_model::memory_order_release, atomics::memory_order_relaxed )) {
// slot can be disposed
gc::template retire<disposer>( slot.ptr() );
m_Stat.onUpdateExisting();
}
// the slot must be expanded
- atomic_node_ptr * pNewArr;
- size_t nNewSlot;
- std::tie( pNewArr, nNewSlot ) = expand_slot( pArr[ nSlot ], slot, splitter );
- if ( pNewArr ) {
- pArr = pNewArr;
- nSlot = nNewSlot;
- }
+ expand_slot( pArr[ nSlot ], slot, nOffset );
}
else {
// the slot is empty, try to insert data node
if ( pArr[nSlot].compare_exchange_strong( pNull, node_ptr( &val ), memory_model::memory_order_release, atomics::memory_order_relaxed ))
{
// the new data node has been inserted
- f( val );
++m_ItemCounter;
m_Stat.onUpdateNew();
return std::make_pair( true, true );
The function returns \p true if success and \p false otherwise.
*/
- bool unlink( value_type& val )
+ bool unlink( value_type const& val )
{
typename gc::Guard guard;
auto pred = [&val](value_type const& item) -> bool { return &item == &val; };
*/
guarded_ptr extract( hash_type const& hash )
{
- typename gc::Guard guard;
- value_type * p = do_erase( hash, guard, []( value_type const&) -> bool {return true; } );
-
- // p is guarded by HP
- if ( p ) {
- gc::template retire<disposer>( p );
- --m_ItemCounter;
- m_Stat.onEraseSuccess();
- return guarded_ptr(p);
+ guarded_ptr gp;
+ {
+ typename gc::Guard guard;
+ value_type * p = do_erase( hash, guard, []( value_type const&) -> bool {return true; } );
+
+ // p is guarded by HP
+ if ( p ) {
+ gc::template retire<disposer>( p );
+ --m_ItemCounter;
+ m_Stat.onEraseSuccess();
+ gp.reset( p );
+ }
}
- return guarded_ptr();
+ return gp;
}
/// Finds an item by it's \p hash
*/
guarded_ptr get( hash_type const& hash )
{
- typename gc::Guard guard;
- value_type * p = search( hash, guard );
-
- // p is guarded by HP
- if ( p )
- return guarded_ptr( p );
- return guarded_ptr();
+ guarded_ptr gp;
+ {
+ typename gc::Guard guard;
+ gp.reset( search( hash, guard ));
+ }
+ return gp;
}
/// Clears the set (non-atomic)
if ( array_bits < 2 )
array_bits = 2;
- if ( head_bits < 8 )
- head_bits = 8;
+ if ( head_bits < 4 )
+ head_bits = 4;
if ( head_bits > hash_bits )
head_bits = hash_bits;
if ( (hash_bits - head_bits) % array_bits != 0 )
else {
// data node
if ( pArr->compare_exchange_strong( slot, node_ptr(), memory_model::memory_order_acquire, atomics::memory_order_relaxed )) {
- gc::template retire<disposer>( slot.ptr() );
- --m_ItemCounter;
- m_Stat.onEraseSuccess();
+ if ( slot.ptr() ) {
+ gc::template retire<disposer>( slot.ptr() );
+ --m_ItemCounter;
+ m_Stat.onEraseSuccess();
+ }
break;
}
}
{
return converter( p ).pArr;
}
- static node_ptr * to_node( atomic_node_ptr * p )
+ static value_type * to_node( atomic_node_ptr * p )
{
return converter( p ).pData;
}
- std::pair< atomic_node_ptr *, size_t > expand_slot( atomic_node_ptr& slot, node_ptr current, hash_splitter& splitter )
+ bool expand_slot( atomic_node_ptr& slot, node_ptr current, size_t nOffset )
{
+ assert( current.bits() == 0 );
+ assert( current.ptr() );
+
+ size_t idx = hash_splitter(hash_accessor()(*current.ptr()), nOffset).cut( m_Metrics.array_node_size_log );
+ atomic_node_ptr * pArr = alloc_array_node();
+
node_ptr cur(current.ptr());
if ( !slot.compare_exchange_strong( cur, cur | array_converting, memory_model::memory_order_release, atomics::memory_order_relaxed )) {
m_Stat.onExpandNodeFailed();
- return std::make_pair(static_cast<atomic_node_ptr *>(nullptr), size_t(0));
+ free_array_node( pArr );
+ return false;
}
- atomic_node_ptr * pArr = alloc_array_node();
- size_t idx = splitter.cut( m_Metrics.array_node_size_log );
pArr[idx].store( current, memory_model::memory_order_release );
cur = cur | array_converting;
slot.compare_exchange_strong( cur, node_ptr( to_node( pArr ), array_node ), memory_model::memory_order_release, atomics::memory_order_relaxed )
);
- return std::make_pair( pArr, idx );
+ m_Stat.onArrayNodeCreated();
+ return true;
}
value_type * search( hash_type const& hash, typename gc::Guard& guard )
}
else if ( slot.ptr() && cmp( hash, hash_accessor()( *slot.ptr() )) == 0 ) {
// item found
- m_Stat.onFindSucces();
+ m_Stat.onFindSuccess();
return slot.ptr();
}
m_Stat.onFindFailed();
Microsoft Visual Studio Solution File, Format Version 12.00\r
# Visual Studio Express 2013 for Windows Desktop\r
-VisualStudioVersion = 12.0.31101.0\r
+VisualStudioVersion = 12.0.40629.0\r
MinimumVisualStudioVersion = 10.0.40219.1\r
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "cds", "cds.vcxproj", "{408FE9BC-44F0-4E6A-89FA-D6F952584239}"\r
EndProject\r
..\..\..\tests\unit\print_cuckoo_stat.h = ..\..\..\tests\unit\print_cuckoo_stat.h\r
..\..\..\tests\unit\print_ellenbintree_stat.h = ..\..\..\tests\unit\print_ellenbintree_stat.h\r
..\..\..\tests\unit\print_mspriorityqueue_stat.h = ..\..\..\tests\unit\print_mspriorityqueue_stat.h\r
+ ..\..\..\tests\unit\print_multilevel_hashset_stat.h = ..\..\..\tests\unit\print_multilevel_hashset_stat.h\r
..\..\..\tests\unit\print_segmentedqueue_stat.h = ..\..\..\tests\unit\print_segmentedqueue_stat.h\r
..\..\..\tests\unit\print_skip_list_stat.h = ..\..\..\tests\unit\print_skip_list_stat.h\r
..\..\..\tests\unit\print_split_list_stat.h = ..\..\..\tests\unit\print_split_list_stat.h\r
{
unsigned int nDisposeCount ; // count of disposer calling
Hash hash;
+ unsigned int nInsertCall;
+ unsigned int nFindCall;
+ unsigned int nEraseCall;
+
+ Item()
+ : nDisposeCount(0)
+ , nInsertCall(0)
+ , nFindCall(0)
+ , nEraseCall(0)
+ {}
};
template <typename Hash>
}
};
+ struct item_disposer {
+ template <typename Hash>
+ void operator()( Item<Hash> * p )
+ {
+ ++p->nDisposeCount;
+ }
+ };
+
template <typename Set>
void test_hp()
{
- Set s;
+ typedef typename Set::hash_type hash_type;
+ typedef typename Set::value_type value_type;
+
+ std::hash<hash_type> hasher;
+
+ size_t const arrCapacity = 1000;
+ std::vector< value_type > arrValue;
+ arrValue.reserve( arrCapacity );
+ for ( size_t i = 0; i < arrCapacity; ++i ) {
+ arrValue.emplace_back( value_type() );
+ arrValue.back().hash = hasher( i );
+ }
+ CPPUNIT_ASSERT( arrValue.size() == arrCapacity );
+
+ Set s( 4, 2 );
+ CPPUNIT_ASSERT(s.head_size() == 16 );
+ CPPUNIT_ASSERT(s.array_node_size() == 4 );
+
+ // insert() test
+ CPPUNIT_ASSERT(s.size() == 0 );
+ CPPUNIT_ASSERT(s.empty() );
+ for ( auto& el : arrValue ) {
+ CPPUNIT_ASSERT( s.insert( el ));
+ CPPUNIT_ASSERT(s.contains( el.hash ));
+ }
+ CPPUNIT_ASSERT(s.size() == arrCapacity );
+ for ( auto& el : arrValue ) {
+ CPPUNIT_ASSERT(s.contains( el.hash ));
+ CPPUNIT_ASSERT( !s.insert( el ) );
+ }
+ CPPUNIT_ASSERT(s.size() == arrCapacity );
+ CPPUNIT_ASSERT( !s.empty() );
+
+ // update() exists test
+ for ( auto& el : arrValue ) {
+ bool bOp, bInsert;
+ std::tie(bOp, bInsert) = s.update( el, false );
+ CPPUNIT_ASSERT( bOp );
+ CPPUNIT_ASSERT( !bInsert );
+ CPPUNIT_ASSERT( el.nFindCall == 0 );
+ CPPUNIT_ASSERT(s.find(el.hash, [](value_type& v) { v.nFindCall++; } ));
+ CPPUNIT_ASSERT( el.nFindCall == 1 );
+ }
+
+ // unlink test
+ CPPUNIT_ASSERT(s.size() == arrCapacity );
+ for ( auto const& el : arrValue ) {
+ CPPUNIT_ASSERT(s.unlink( el ));
+ CPPUNIT_ASSERT(!s.contains( el.hash ));
+ }
+ CPPUNIT_ASSERT(s.size() == 0 );
+ Set::gc::force_dispose();
+ for ( auto const& el : arrValue ) {
+ CPPUNIT_ASSERT( el.nDisposeCount == 1 );
+ }
+
+ // new hash values
+ for ( auto& el : arrValue )
+ el.hash = hasher( el.hash );
+
+ // insert( func )
+ CPPUNIT_ASSERT(s.size() == 0 );
+ for ( auto& el : arrValue ) {
+ CPPUNIT_ASSERT( s.insert( el, []( value_type& v ) { ++v.nInsertCall; } ));
+ CPPUNIT_ASSERT(s.contains( el.hash ));
+ CPPUNIT_ASSERT( el.nInsertCall == 1 );
+ }
+ CPPUNIT_ASSERT(s.size() == arrCapacity );
+ for ( auto& el : arrValue ) {
+ CPPUNIT_ASSERT(s.contains( el.hash ));
+ CPPUNIT_ASSERT( !s.insert( el ) );
+ }
+ CPPUNIT_ASSERT(s.size() == arrCapacity );
+ CPPUNIT_ASSERT( !s.empty() );
+
+ for ( auto& el : arrValue )
+ el.nDisposeCount = 0;
+
+ s.clear();
+ CPPUNIT_ASSERT(s.size() == 0 );
+ Set::gc::force_dispose();
+ for ( auto const& el : arrValue ) {
+ CPPUNIT_ASSERT( el.nDisposeCount == 1 );
+ }
+
+ // new hash values
+ for ( auto& el : arrValue )
+ el.hash = hasher( el.hash );
+
+ // update test
+ for ( auto& el : arrValue ) {
+ bool bOp, bInsert;
+ std::tie(bOp, bInsert) = s.update( el, false );
+ CPPUNIT_ASSERT( !bOp );
+ CPPUNIT_ASSERT( !bInsert );
+ CPPUNIT_ASSERT( !s.contains( el.hash ));
+
+ std::tie(bOp, bInsert) = s.update( el, true );
+ CPPUNIT_ASSERT( bOp );
+ CPPUNIT_ASSERT( bInsert );
+ CPPUNIT_ASSERT( s.contains( el.hash ));
+ }
+ CPPUNIT_ASSERT(s.size() == arrCapacity );
+
+ // erase test
+ for ( auto& el : arrValue ) {
+ el.nDisposeCount = 0;
+ CPPUNIT_ASSERT( s.contains( el.hash ));
+ CPPUNIT_ASSERT(s.erase( el.hash ));
+ CPPUNIT_ASSERT( !s.contains( el.hash ));
+ CPPUNIT_ASSERT( !s.erase( el.hash ));
+ }
+ CPPUNIT_ASSERT(s.size() == 0 );
+ Set::gc::force_dispose();
+ for ( auto& el : arrValue ) {
+ CPPUNIT_ASSERT( el.nDisposeCount == 1 );
+ CPPUNIT_ASSERT(s.insert( el ));
+ }
+
+ // erase with functor, get() test
+ for ( auto& el : arrValue ) {
+ el.nDisposeCount = 0;
+ CPPUNIT_ASSERT( s.contains( el.hash ) );
+ {
+ typename Set::guarded_ptr gp{ s.get( el.hash ) };
+ CPPUNIT_ASSERT( gp );
+ CPPUNIT_ASSERT( gp->nEraseCall == 0);
+ CPPUNIT_ASSERT(s.erase( gp->hash, []( value_type& i ) { ++i.nEraseCall; } ));
+ CPPUNIT_ASSERT( gp->nEraseCall == 1);
+ Set::gc::force_dispose();
+ CPPUNIT_ASSERT( gp->nDisposeCount == 0 );
+ }
+ CPPUNIT_ASSERT( !s.contains( el.hash ));
+ CPPUNIT_ASSERT( !s.erase( el.hash ));
+ CPPUNIT_ASSERT( el.nEraseCall == 1 );
+ Set::gc::force_dispose();
+ CPPUNIT_ASSERT( el.nDisposeCount == 1 );
+ }
+ CPPUNIT_ASSERT(s.size() == 0 );
+
+ // new hash values
+ for ( auto& el : arrValue ) {
+ el.hash = hasher( el.hash );
+ el.nDisposeCount = 0;
+ bool bOp, bInsert;
+ std::tie(bOp, bInsert) = s.update( el );
+ CPPUNIT_ASSERT( bOp );
+ CPPUNIT_ASSERT( bInsert );
+ }
+ CPPUNIT_ASSERT(s.size() == arrCapacity );
+
+ // extract test
+ for ( auto& el : arrValue ) {
+ CPPUNIT_ASSERT( s.contains( el.hash ) );
+ typename Set::guarded_ptr gp = s.extract( el.hash );
+ CPPUNIT_ASSERT( gp );
+ Set::gc::force_dispose();
+ CPPUNIT_ASSERT( el.nDisposeCount == 0 );
+ CPPUNIT_ASSERT( gp->nDisposeCount == 0 );
+ gp = s.get( el.hash );
+ CPPUNIT_ASSERT( !gp );
+ Set::gc::force_dispose();
+ CPPUNIT_ASSERT( el.nDisposeCount == 1 );
+ CPPUNIT_ASSERT( !s.contains( el.hash ) );
+ }
+ CPPUNIT_ASSERT(s.size() == 0 );
+ CPPUNIT_ASSERT(s.empty() );
+
+ CPPUNIT_MSG( s.statistics() );
}
void hp_stdhash();
+ void hp_stdhash_stat();
CPPUNIT_TEST_SUITE(IntrusiveMultiLevelHashSetHdrTest)
CPPUNIT_TEST(hp_stdhash)
+ CPPUNIT_TEST(hp_stdhash_stat)
CPPUNIT_TEST_SUITE_END()
};
} // namespace set
#include "set/hdr_intrusive_multilevel_hashset.h"
#include <cds/intrusive/multilevel_hashset_hp.h>
+#include "unit/print_multilevel_hashset_stat.h"
namespace set {
namespace {
struct traits: public ci::multilevel_hashset::traits
{
typedef get_hash<hash_type> hash_accessor;
+ typedef item_disposer disposer;
};
typedef ci::MultiLevelHashSet< gc_type, Item<hash_type>, traits > set_type;
test_hp<set_type>();
Item<hash_type>,
typename ci::multilevel_hashset::make_traits<
ci::multilevel_hashset::hash_accessor< get_hash<hash_type>>
+ , ci::opt::disposer< item_disposer >
>::type
> set_type2;
test_hp<set_type2>();
}
+
+ void IntrusiveMultiLevelHashSetHdrTest::hp_stdhash_stat()
+ {
+ typedef size_t hash_type;
+
+ struct traits: public ci::multilevel_hashset::traits
+ {
+ typedef get_hash<hash_type> hash_accessor;
+ typedef item_disposer disposer;
+ typedef ci::multilevel_hashset::stat<> stat;
+ };
+ typedef ci::MultiLevelHashSet< gc_type, Item<hash_type>, traits > set_type;
+ test_hp<set_type>();
+
+ typedef ci::MultiLevelHashSet<
+ gc_type,
+ Item<hash_type>,
+ typename ci::multilevel_hashset::make_traits<
+ ci::multilevel_hashset::hash_accessor< get_hash<hash_type>>
+ , ci::opt::disposer< item_disposer >
+ ,co::stat< ci::multilevel_hashset::stat<>>
+ >::type
+ > set_type2;
+ test_hp<set_type2>();
+ }
+
} // namespace set
CPPUNIT_TEST_SUITE_REGISTRATION(set::IntrusiveMultiLevelHashSetHdrTest);
--- /dev/null
+//$$CDS-header$$
+
+#ifndef CDSUNIT_PRINT_MULTILEVEL_HASHSET_STAT_H
+#define CDSUNIT_PRINT_MULTILEVEL_HASHSET_STAT_H
+
+#include <cds/intrusive/details/multilevel_hashset_base.h>
+#include <ostream>
+
+namespace std {
+
+ static inline ostream& operator <<( ostream& o, cds::intrusive::multilevel_hashset::stat<> const& s )
+ {
+ return
+ o << "Stat [cds::intrusive::multilevel_hashset::stat]\n"
+ << "\t\t m_nInsertSuccess: " << s.m_nInsertSuccess.get() << "\n"
+ << "\t\t m_nInsertFailed: " << s.m_nInsertFailed.get() << "\n"
+ << "\t\t m_nInsertRetry: " << s.m_nInsertRetry.get() << "\n"
+ << "\t\t m_nUpdateNew: " << s.m_nUpdateNew.get() << "\n"
+ << "\t\t m_nUpdateExisting: " << s.m_nUpdateExisting.get() << "\n"
+ << "\t\t m_nUpdateFailed: " << s.m_nUpdateFailed.get() << "\n"
+ << "\t\t m_nUpdateRetry: " << s.m_nUpdateRetry.get() << "\n"
+ << "\t\t m_nEraseSuccess: " << s.m_nEraseSuccess.get() << "\n"
+ << "\t\t m_nEraseFailed: " << s.m_nEraseFailed.get() << "\n"
+ << "\t\t m_nEraseRetry: " << s.m_nEraseRetry.get() << "\n"
+ << "\t\t m_nFindSuccess: " << s.m_nFindSuccess.get() << "\n"
+ << "\t\t m_nFindFailed: " << s.m_nFindFailed.get() << "\n"
+ << "\t\t m_nExpandNodeSuccess: " << s.m_nExpandNodeSuccess.get() << "\n"
+ << "\t\t m_nExpandNodeFailed: " << s.m_nExpandNodeFailed.get() << "\n"
+ << "\t\t m_nSlotChanged: " << s.m_nSlotChanged.get() << "\n"
+ << "\t\t m_nSlotConverting: " << s.m_nSlotConverting.get() << "\n"
+ << "\t\t m_nArrayNodeCount: " << s.m_nArrayNodeCount.get() << "\n";
+ }
+
+ static inline ostream& operator <<( ostream& o, cds::intrusive::multilevel_hashset::empty_stat const& /*s*/ )
+ {
+ return o;
+ }
+
+} // namespace std
+
+#endif // #ifndef CDSUNIT_PRINT_MULTILEVEL_HASHSET_STAT_H