views:

191

answers:

4

Hi all,

I've written a low lock list in C++ on windows 32-bit. I'm getting BIG improvements over using critical sections but I'd like someone to sanity check that what I'm doing is correct and there aren't any errors in what I've done:

#ifndef __LOW_LOCK_STACK_H_
#define __LOW_LOCK_STACK_H_

template< class T > class LowLockStack
{
protected:
    struct Entry
    {
        Entry*  pNext;
        T*              pData;
    };
    union Header
    {
        __int64         m_XChg;
        struct
        {
                Entry*  m_pNext;
                __int16 m_Depth;
                __int16 m_Counter;
        };
    };

    Header      m_Header;
public:
    LowLockStack()
    {
        m_Header.m_pNext        = NULL;
        m_Header.m_Depth        = 0;
        m_Header.m_Counter  = 0;
    }

    ~LowLockStack()
    {
    }

    void PushEntry( T* pData )
    {
        Entry* pEntry   = new Entry;
        pEntry->pData   = pData;

        Header header;
        Header xchg;
        do
        {
            xchg.m_XChg   = m_Header.m_XChg;

            header.m_pNext  = pEntry;
            header.m_Depth  = xchg.m_Depth + 1;
            header.m_Counter = xchg.m_Counter + 1;
            pEntry->pNext  = xchg.m_pNext;
        } while( _InterlockedCompareExchange64( &m_Header.m_XChg, header.m_XChg, xchg.m_XChg ) != xchg.m_XChg );
    }

    T* PopEntry()
    {
        Entry* pEntry   = NULL;
        Header header;
        Header xchg;
        do
        {
            xchg.m_XChg    = m_Header.m_XChg;

            pEntry                  = xchg.m_pNext;
            if ( pEntry == NULL )
            {
                 return NULL;
            }

            header.m_pNext  = pEntry->pNext;
            header.m_Depth  = xchg.m_Depth - 1;

        } while( _InterlockedCompareExchange64( &m_Header.m_XChg, header.m_XChg, xchg.m_XChg ) != xchg.m_XChg );

        T* pRet = pEntry->pData;
        delete pEntry;

        return pRet;
    }

    __int32 GetDepth()
    {
        return m_Header.m_Depth;
    }
};

#endif

If there are no errors (which i doubt ;)) then think of it as a reference implementation :D

Edit: I've updated the code taking into account a number of criticisms.

+2  A: 

You do not synchronize access to the list header member. This is bad at least on 2 levels:

  • assigning values to the list header can be not as atomic as you think. Which means that an unsynchronized read operation can potentially get a corrupted value.

  • another, more probable issue with this is that if your box has multiple cores, every one of them can have in the processor cache its own copy of the value. To make them synchronize the values you need a memory barrier

mfeingold
InterlockedCompareExchange64 provides a memory barrier.
Michael
Than this is even worse than it seems - memory barrier is an expensive operation and this implementation will have O(n) of them for the list with n elements
mfeingold
There's typically no escaping memory barriers. An implementation using locks, even in the case of no contention, would still have memory barriers (if locks didn't provide barriers, they'd be useless.)
Michael
Absolutely - but you do not have to have this many of them. My guess one per call should be enough
mfeingold
Ideally it is one per call. The retry is only done if the head node gets changed after fetching the data but before the exchange.
Michael
@Michael BTW in one particular case I found a way to escape the locks (sort of) check this out: http://bit.ly/7NzATx
mfeingold
Well in answer to point 1 .. surely this isn't a major problem as it it will, simply, cause the CompareExchange to fail and it will spin and try again? As for 2 ... yeah CompareExchange IS a memory barrier so i'm not worried about that ...
Goz
@Goz - it may just throw an exception
mfeingold
How could it throw an exception? I think I'm confused as to what you mean then ...
Goz
This is a pointer to some memory location. If you replace one valid pointer with another one both represent a valid memory location, but if you try to use the pointer value while only the first 2 bytes are replaced with the bytes of the new value - there is no guarantee this will be a valid pointer
mfeingold
+4  A: 

As you discovered, lock free programming is very difficult to get right.

Windows already has support for lock-free singly linked lists,http://msdn.microsoft.com/en-us/library/ms684121(VS.85).aspx, you should try using that rather than roll your own.

Michael
No I'm fully aware of it. But the reason I've done this is so that I can easily re-implement for other platforms ... Its more of a learning exercise than anything :)
Goz
+1  A: 

The most obvious error is the name you've given it. Regardless of the fact that you've implemented it as a linked list, what you've implemented is a stack.

Jerry Coffin
Whats wrong with using cmpxchg16b instead? That would solve the 64-bit issues ...
Goz
PLUS good point on it being a stack ... I'll accept that :D
Goz
@Goz:cmpxchg16b is fine, except that VC++ doesn't provide a direct front-end for it and not all processors include it. From VC++, you get InterlockedCompare64Exchange128 instead. I think that's enough for what you're doing right now, but you need to be careful that you only need the comparison on 64 bits, even though you're exchanging 128.
Jerry Coffin
http://msdn.microsoft.com/en-us/library/bb514094.aspx?
Goz
@Goz:Oops -- somehow or other I missed that. Thanks for the pointer.
Jerry Coffin
+1  A: 
Nikolai N Fetissov
OK. Thats a good point. I assume I could easily get round this by making count only 16-bit and then incrementing a number in the high 16-bits each time a pop is made? This way I only run into the problem if 64K items are added while the other thread is spinning which seems a tad unlikely ...
Goz
Yes, but you are just reducing the probability :)
Nikolai N Fetissov
Point taken ... btw I've implemented herb sutter's approach. I found the critical section based system out performed it ... I was rather unimpressed, ESPECIALLY, given how impressed I **Usually** am with Mr Sutter ...
Goz
Looking at Microsoft's SList .. they appear to be doing what I suggested above ... I should properly backward engineer their code to see what they do though ...
Goz