C ++ 0x атомарная реализация в c ++ 98 вопрос о __sync_synchronize () - PullRequest
7 голосов
/ 11 марта 2010

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

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

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

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

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

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

М.

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

};

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

Ответы [ 2 ]

5 голосов
/ 25 марта 2010

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

volatile T obj;

энергозависимость здесь бесполезна, тем более, что вы сами делаете все барьеры.

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

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

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

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

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

0 голосов
/ 11 марта 2010
inline operator T()
{
    __sync_synchronize();   // Not sure this is overkill
    return obj;
}

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

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

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

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

#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_int», который просто гарантирует наличие как минимум 4 байта (если вы поддерживаете 32-битные и 64-битные системы) и «atomic_pointer», если вам это нужно. Таким образом, имя класса также подразумевает семантику и цель.

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


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

Чтобы компилятор и процессор не переупорядочивали операции чтения / записи, вам нужна __sync_synchronize ().

Но учтите, что семантика получения / выпуска может дать лучшую производительность, чем полные барьеры.


Edit2:

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

Что вы хотите предотвратить переупорядочением? В большинстве случаев вы хотите написать это:

    obj = val;
    __sync_synchronize();

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

...