【问题标题】:C++0x atomic implementation in c++98 question about __sync_synchronize()c++98中关于__sync_synchronize()的C++0x原子实现问题
【发布时间】:2011-01-26 06:50:00
【问题描述】:

我编写了以下原子模板,以模仿即将在即将推出的 c++0x 标准中提供的原子操作。

但是,我不确定在返回基础值时进行的 __sync_synchronize() 调用是否必要。

据我了解,__sync_synchronize() 是一个完整的内存屏障,我不确定在返回对象值时是否需要如此昂贵的调用。

我很确定围绕值的设置需要它,但我也可以使用程序集来实现它..

__asm__ __volatile__ ( "rep;nop": : :"memory" );

有谁知道我在返回对象时肯定需要 synchronize()。

M.

template < typename T >
struct atomic
{
private:
    volatile T obj;

public:
    atomic( const T & t ) :
        obj( t )
    {
    }

    inline operator T()
    {
        __sync_synchronize();   // Not sure this is overkill
        return obj;
    }

    inline atomic< T > & operator=( T val )
    {
        __sync_synchronize();   // Not sure if this is overkill
        obj = val;
        return *this;
    }

    inline T operator++()
    {
        return __sync_add_and_fetch( &obj, (T)1 );
    }

    inline T operator++( int )
    {
        return __sync_fetch_and_add( &obj, (T)1 );
    }

    inline T operator+=( T val )
    {
        return __sync_add_and_fetch( &obj, val );
    }

    inline T operator--()
    {
        return __sync_sub_and_fetch( &obj, (T)1 );
    }

    inline T operator--( int )
    {
        return __sync_fetch_and_sub( &obj, (T)1 );
    }

    inline T operator-=( T )
    {
        return __sync_sub_and_fetch( &obj, val );
    }

    // Perform an atomic CAS operation
    // returning the value before the operation
    inline T exchange( T oldVal, T newVal )
    {
        return __sync_val_compare_and_swap( &obj, oldval, newval );
    }

};

更新:由于编译器优化,我想确保操作在面对读/写重新排序时保持一致。

【问题讨论】:

  • __sync_synchronize() 来自哪里?该名称是为实现保留的,所以它是您的编译器的名称吗?
  • @MSalters:它是一个完整的内存屏障内在,由 GCC 提供
  • @jalf。但是,它在我的 GCC (4.1.2) 版本中被破坏并产生无操作。我正在考虑通过 asm() 提供我自己的。 (x86 上的 sfence/lfence/mfence,solaris 上的???)。
  • 仅供参考。 Solaris 对 sfence、lfence 和 mfence 分别使用“membar #LoadStore”、“membar #LoadLoad”和“membar #MemIssue”

标签: c++ templates c++11 atomic


【解决方案1】:

首先,一些琐碎的评论:

volatile T obj;

volatile 在这里是没用的,更何况你自己设置了所有的障碍。

inline T operator++( int )

不需要内联,因为在类中定义方法时会隐含它。

getter 和 setter:

inline operator T()
{
    __sync_synchronize();   // (I)
    T tmp=obj;
    __sync_synchronize();   // (II)
    return tmp;
}

inline atomic< T > & operator=( T val )
{
    __sync_synchronize();   // (III)
    obj = val;
    __sync_synchronize();   // (IV)
    return *this;
}

为了确保读取和写入内存访问的总顺序,每次访问都需要两个屏障(像这样)。我会对只有障碍 (II) 和 (III) 感到满意,因为它们足以满足我提出的某些用途(例如,指针/布尔表示数据在那里,自旋锁),但是,除非另有说明,否则我不会省略其他,因为有人可能需要它们(如果有人证明您可以在不限制可能用途的情况下省略一些障碍,那就太好了,但我认为这是不可能的)。

当然,这将是不必要的复杂和缓慢。

也就是说,我只是转储障碍,甚至是在类似模板的任何地方使用障碍的想法。请注意:

  • 该接口的排序语义全部由您定义;如果你决定界面在这里或那里有障碍,他们必须在这里或那里,期间。如果您不定义它,您可以提出更有效的设计,因为特定问题可能不需要所有障碍,甚至不是全部障碍。
  • 通常,如果您有一个可以给您带来性能优势的无锁算法,您可以使用原子;这意味着过早地悲观访问的接口可能无法用作它的构建块,因为它会影响性能本身。
  • 无锁算法通常包含无法由一种原子数据类型封装的通信,因此您需要知道算法中发生了什么才能将屏障准确放置在它们所属的位置(例如,在实现锁时,您需要一个屏障你获得它之后,但你释放它之前,这两者都是写入,至少在原则上)
  • 如果您不想遇到问题,并且不确定在算法中明确放置障碍,请使用基于锁的算法。没什么不好的。

顺便说一句,c++0x 接口允许您指定精确的内存排序约束。

【讨论】:

    【解决方案2】:
    inline operator T()
    {
        __sync_synchronize();   // Not sure this is overkill
        return obj;
    }
    

    短版:这太过分了。

    长版:

    你为什么要把这个类实现为一个模板呢?这是没有意义的,因为原子操作只允许对 1-8 字节的整数类型进行,您甚至无法确定所有平台都支持 8 字节整数。

    您应该将原子类实现为非模板版本,并使用硬件/系统的“本机”整数类型。这是 32 位处理器/操作系统上的 int32_t 和 64 位系统上的 int64_t。例如:

    #ifdef ...
    typedef ... native_int_type;
    #endif
    // verify that you choosed the correct integer type
    BOOST_STATIC_ASSERT(sizeof(native_int_type) == sizeof(void*));
    

    BOOST_STATIC_ASSERT 直接指向 C++0x 中的“static_assert()”。

    如果您使用“完美匹配”整数类型,您可以像这样编写运算符:

    operator native_int_type() { return obj; }
    

    因为 obj 是易失的,所以保证获取值而不返回任何缓存值。而且因为您使用的是“本机”整数类型,所以可以确定读取这样的值是原子的。

    atomic& operator=( native_integer_type val )
    

    同样,如果您使用正确的整数类型,则不需要同步。在 intel 32 位系统上读取/设置 int32 是原子的,在 64 位系统上读取/设置 int64 也是原子性的。

    我认为将 atomic 实现为模板没有任何好处。原子操作依赖于平台。最好提供一个“atomic_int”类,它只保证至少有 4 个字节(如果您支持 32 位和 64 位系统)和一个“atomic_pointer”(如果需要)。这样,类的名称也暗示了语义和目的。

    如果您只使用“原子”,那么人们可能会想:“哇,我只需要将我的字符串类放在这个模板中,然后它就是线程安全的!”。


    编辑:回答您的更新:“由于编译器优化,我想确保操作在面对读/写重新排序时保持一致。”

    为了防止编译器和 cpu 重新排序读/写操作,您需要 __sync_synchronize()。

    但请注意,获取/释放语义可能比完全屏障产生更好的性能。


    编辑2:

    inline atomic< T > & operator=( T val )
    {
        __sync_synchronize();   // Not sure if this is overkill
        obj = val;
        return *this;
    }
    

    您希望防止重新排序的内容是什么?在大多数情况下,你想这样写:

        obj = val;
        __sync_synchronize();
    

    相反。因为您想确保在从函数返回后写入值。

    【讨论】:

    • 一个操作是原子的并不意味着内存屏障是不必要的。没有它们,读/写可能会被缓存并延迟一段未指定的时间。或者它可能会被重新排序,从而改变代码的语义。
    • @neverlord。我同意您对将原子操作实现为模板的评价,我这样做的唯一原因是,据我了解,c++0x 将其原子定义为“原子”等。我同意有人查看代码很可能认为“原子”是有效的。但是,我要确保为“T”提交的类型是整数类型,并且在主机允许的范围内。我还没有走那么远:o)
    • @jalf。这就是我开车的目的。我想确保在优化时该值是一致的。你是说这个额外的“__synchronize()”是必要的吗?
    • @jalf。对 volatile 变量的读/写操作不会被缓存。但在某些情况下,重新排序可能是个问题。我认为解决这个问题的一个好方法是提供几种具有不同重新排序语义的原子操作的实现,就像 QT 所做的那样:doc.trolltech.com/4.6/qatomicint.html 但在这种情况下,您不需要 __sync_synchronize() 来验证读/写是原子的。跨度>
    • 我会这么说,是的。没有它,编译器可以内联类中的所有内容,然后重新排序可能会在并发访问类期间改变行为的操作。或者即使编译器做了正确的事情,CPU 也可能缓存读/写操作。这可能取决于其他 __sync 原语是否也隐式定义了内存屏障。如果他们这样做,它可能是不必要的。检查文档,我猜。 :)
    猜你喜欢
    • 2010-10-06
    • 1970-01-01
    • 2021-03-03
    • 1970-01-01
    • 2022-06-13
    • 1970-01-01
    • 1970-01-01
    • 2011-03-08
    • 1970-01-01
    相关资源
    最近更新 更多