Атомная реализация С++ 0x в вопросе С++ 98 о __sync_synchronize()

Я написал следующий атомарный шаблон с целью имитации атомарных операций, которые будут доступны в будущем стандарте С++ 0x.

Однако я не уверен, что вызов __sync_synchronize(), который у меня есть для возврата базового значения, необходим.

Насколько я понимаю, __sync_synchronize() - это полный барьер памяти, и я не уверен, что мне нужен такой дорогостоящий вызов при возврате значения объекта.

Я почти уверен, что это понадобится для настройки значения, но я также мог бы реализовать это с помощью сборки.

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

Кто-нибудь знает, нужна ли мне синхронизация() при возврате объекта.

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 );
    }

};

Обновление: я хочу убедиться, что операции согласуются с изменением порядка чтения/записи из-за оптимизации компилятора.


person ScaryAardvark    schedule 11.03.2010    source источник
comment
Откуда __sync_synchronize()? Имя зарезервировано для реализации, значит, оно принадлежит вашему компилятору?   -  person MSalters    schedule 11.03.2010
comment
@MSalters: это встроенный полный барьер памяти, предоставляемый GCC.   -  person jalf    schedule 11.03.2010
comment
@jalf. Однако в моей версии GCC (4.1.2) он не работает и выдает no-op. Я смотрю на предоставление своего собственного через asm(). (sfence/lfence/mfence на x86, ??? на Solaris).   -  person ScaryAardvark    schedule 11.03.2010
comment
просто для справки. Solaris использует membar #LoadStore, membar #LoadLoad и membar #MemIssue для sfence, lfence и mfence соответственно.   -  person ScaryAardvark    schedule 12.03.2010


Ответы (2)


Сначала несколько мелких замечаний:

volatile T obj;

volatile тут ни к чему, тем более, что все барьеры вы делаете сами.

inline T operator++( int )

inline не нужен, так как он подразумевается, когда метод определен внутри класса.

Геттеры и сеттеры:

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), поскольку их достаточно для некоторых применений, которые я придумал (например, указатели/логические данные о наличии данных, спин-блокировка), но, если не указано иное, я бы не пропустил другие , потому что они могут кому-то понадобиться (было бы неплохо, если бы кто-нибудь показал, что можно опустить некоторые барьеры, не ограничивая возможности использования, но я не думаю, что это возможно).

Конечно, это было бы излишне сложно и медленно.

Тем не менее, я бы просто сбросил барьеры и даже идею использования барьеров в любом месте аналогичного шаблона. Обратите внимание, что:

  • семантика упорядочивания этого интерфейса определяется вами; и если вы решите, что интерфейс имеет барьеры здесь или там, они должны быть здесь или там, и точка. Если вы не определите его, вы сможете придумать более эффективный дизайн, потому что не все барьеры или даже не полные барьеры могут понадобиться для конкретной проблемы.
  • обычно вы используете atomics, если у вас есть алгоритм без блокировок, который может дать вам преимущество в производительности; это означает, что интерфейс, который преждевременно пессимизирует доступ, вероятно, будет непригоден для использования в качестве его строительного блока, поскольку он будет препятствовать самой производительности.
  • алгоритмы без блокировок обычно содержат обмен данными, который не может быть инкапсулирован одним атомарным типом данных, поэтому вам нужно знать, что происходит в алгоритме, чтобы размещать барьеры именно там, где они должны быть (например, при реализации блокировки вам нужен барьер после его приобретения, но до его выпуска, что является записью, по крайней мере, в принципе)
  • если вы не хотите иметь проблем и не уверены в явном размещении барьеров в алгоритме, просто используйте алгоритмы на основе блокировки. В этом нет ничего плохого.

Кстати, интерфейс С++ 0x позволяет указать точные ограничения упорядочения памяти.

person jpalecek    schedule 24.03.2010

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

Краткая версия: это излишество.

Длинная версия:

Почему вы вообще хотите реализовать этот класс как шаблон? Это не имеет смысла, потому что атомарные операции разрешены только для целочисленных типов от 1 до 8 байт, и вы даже не можете быть уверены, что 8-байтовое целое число поддерживается на всех платформах.

Вы должны реализовать свой атомарный класс как версию без шаблона и использовать «родной» целочисленный тип вашего оборудования/системы. Это int32_t на 32-битных процессорах/ОС и int64_t на 64-битных системах. например.:

#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 напрямую связан с "static_assert()" из C++0x.

Если вы используете целочисленный тип "идеально подходит", вы можете написать оператор так:

operator native_int_type() { return obj; }

Поскольку obj является изменчивым, он гарантированно извлечет значение и не вернет никакого кэшированного значения. И поскольку вы используете «родной» целочисленный тип, вы можете быть уверены, что чтение такого значения является атомарным.

atomic& operator=( native_integer_type val )

Опять же, вам не нужна синхронизация, если вы используете правильный целочисленный тип. Чтение/установка int32 в 32-битной системе Intel является атомарной, как и чтение/установка int64 в 64-битной системе.

Я не вижу никакой выгоды от реализации atomic в качестве шаблона. Атомарные операции зависят от платформы. Лучше предложить класс «atomic_int», который просто гарантирует наличие не менее 4 байт (если вы поддерживаете 32-битные и 64-битные системы) и «atomic_pointer», если он вам нужен. Таким образом, имя класса также подразумевает семантику и цель.

Если вы просто используете «atomic», то можно подумать: «Вау, мне просто нужно поместить свой строковый класс в этот шаблон, и тогда он будет потокобезопасным!».


Изменить: чтобы ответить на ваше обновление: «Я хочу убедиться, что операции согласованы перед лицом переупорядочения чтения/записи из-за оптимизации компилятора».

Чтобы компилятор и процессор не переупорядочивали операции чтения/записи, вам нужна функция __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();

вместо. Потому что вы хотите быть уверены, что значение записано, как только вы вернетесь из функции.

person neverlord    schedule 11.03.2010
comment
Тот факт, что операция является атомарной, не означает, что барьеры памяти не нужны. Без них операции чтения/записи могут кэшироваться и задерживаться на неопределенное время. Или он может быть переупорядочен, что изменит семантику вашего кода. - person jalf; 11.03.2010
comment
@ниверлорд. Я согласен с вашей оценкой реализации атомарных операций в качестве шаблонов, и единственная причина, по которой я это сделал, заключалась в том, что, насколько я понимаю, С++ 0x определяет атомарность как атомарность‹ int › и т. д. Я согласен, что кто-то, глядя на код, вполне может думаю, что атомная‹ строка › действительна. Однако я собирался удостовериться, что тип, переданный для T, будет целочисленным типом и будет находиться в пределах диапазона, разрешенного для хоста. Я просто еще не дошел до этого :о) - person ScaryAardvark; 11.03.2010
comment
@jalf. Это то, к чему я вел. Я хочу убедиться, что значение непротиворечиво перед оптимизацией. Вы говорите, что эта дополнительная __synchronize() необходима? - person ScaryAardvark; 11.03.2010
comment
@jalf. Операции чтения/записи для volatile-переменных не кэшируются. Но изменение порядка может быть проблемой при некоторых обстоятельствах. Я думаю, что хороший способ справиться с этим — предоставить несколько реализаций атомарных операций с различной семантикой переупорядочения, как это делает QT: doc.trolltech.com/4.6/qatomicint.html Но вам не нужна __sync_synchronize(), чтобы убедиться, что в этом случае чтение/запись является атомарным. - person neverlord; 11.03.2010
comment
Я бы сказал так, да. Без него компилятор мог бы встроить все в класс, а затем переупорядочить операции, которые могли бы изменить поведение во время параллельного доступа к классу. Или ЦП может кэшировать операции чтения/записи, даже если компилятор делает все правильно. Это может зависеть от того, определяют ли другие примитивы __sync также неявно барьер памяти. Если да, то возможно в этом нет необходимости. Проверьте документы, я думаю. :) - person jalf; 11.03.2010
comment
Я считаю, что std::atomic‹std::string› допустим в С++ 0x. std::atomic‹› должен возвращаться к мьютексам, когда процессору не хватает инструкций (каждому процессору не хватает инструкций для операций с атомарными строками). - person deft_code; 13.03.2010
comment
@ниверлорд. Чтение/запись на оборудовании x86 не всегда гарантируется атомарностью. Если целое число находится в невыровненной памяти, то это определенно не будет атомарной операцией. - person ScaryAardvark; 17.03.2010