Edit: The code here still has some bugs in it, and it could do better in the performance department, but instead of trying to fix this, for the record I took the problem over to the Intel discussion groups and got lots of great feedback, and if all goes well a polished version of Atomic float will be included in a near future release of Intel's Threading Building Blocks
Хорошо, вот сложный вопрос, я хочу, чтобы Atomic float не для сверхбыстрой графической производительности, а для регулярного использования в качестве членов данных классов. И я не хочу платить за использование блокировок для этих классов, потому что это не дает дополнительных преимуществ для моих нужд.
Теперь с Intel tbb и другими атомными библиотеками, которые я видел, поддерживаются целые типы, но не с плавающей запятой. Итак, я продолжил и реализовал один, и он работает ... но я не уверен, ДЕЙСТВИТЕЛЬНО ли он работает, или мне просто очень повезло, что он работает.
Кто-нибудь здесь знает, не является ли это какой-то поточной ересью?
typedef unsigned int uint_32;
struct AtomicFloat
{
private:
tbb::atomic<uint_32> atomic_value_;
public:
template<memory_semantics M>
float fetch_and_store( float value )
{
const uint_32 value_ = atomic_value_.tbb::atomic<uint_32>::fetch_and_store<M>((uint_32&)value);
return reinterpret_cast<const float&>(value_);
}
float fetch_and_store( float value )
{
const uint_32 value_ = atomic_value_.tbb::atomic<uint_32>::fetch_and_store((uint_32&)value);
return reinterpret_cast<const float&>(value_);
}
template<memory_semantics M>
float compare_and_swap( float value, float comparand )
{
const uint_32 value_ = atomic_value_.tbb::atomic<uint_32>::compare_and_swap<M>((uint_32&)value,(uint_32&)compare);
return reinterpret_cast<const float&>(value_);
}
float compare_and_swap(float value, float compare)
{
const uint_32 value_ = atomic_value_.tbb::atomic<uint_32>::compare_and_swap((uint_32&)value,(uint_32&)compare);
return reinterpret_cast<const float&>(value_);
}
operator float() const volatile // volatile qualifier here for backwards compatibility
{
const uint_32 value_ = atomic_value_;
return reinterpret_cast<const float&>(value_);
}
float operator=(float value)
{
const uint_32 value_ = atomic_value_.tbb::atomic<uint_32>::operator =((uint_32&)value);
return reinterpret_cast<const float&>(value_);
}
float operator+=(float value)
{
volatile float old_value_, new_value_;
do
{
old_value_ = reinterpret_cast<float&>(atomic_value_);
new_value_ = old_value_ + value;
} while(compare_and_swap(new_value_,old_value_) != old_value_);
return (new_value_);
}
float operator*=(float value)
{
volatile float old_value_, new_value_;
do
{
old_value_ = reinterpret_cast<float&>(atomic_value_);
new_value_ = old_value_ * value;
} while(compare_and_swap(new_value_,old_value_) != old_value_);
return (new_value_);
}
float operator/=(float value)
{
volatile float old_value_, new_value_;
do
{
old_value_ = reinterpret_cast<float&>(atomic_value_);
new_value_ = old_value_ / value;
} while(compare_and_swap(new_value_,old_value_) != old_value_);
return (new_value_);
}
float operator-=(float value)
{
return this->operator+=(-value);
}
float operator++()
{
return this->operator+=(1);
}
float operator--()
{
return this->operator+=(-1);
}
float fetch_and_add( float addend )
{
return this->operator+=(-addend);
}
float fetch_and_increment()
{
return this->operator+=(1);
}
float fetch_and_decrement()
{
return this->operator+=(-1);
}
};
Спасибо!
Редактировать: изменил size_t на uint32_t, как предложил Грег Роджерс, таким образом, он стал более портативным
Редактировать: добавил листинг для всего этого с некоторыми исправлениями.
Больше правок: Для повышения производительности использование заблокированного числа с плавающей запятой для 5.000.000 + = операций со 100 потоками на моей машине занимает 3,6 секунды, в то время как мой атомарный плавающий элемент даже с его глупым do-while занимает 0,2 секунды, чтобы выполнить ту же работу. Таким образом, повышение производительности> 30x означает, что оно того стоит (и это загвоздка), если оно правильно.
Еще больше правок: Как заметил Авгн, все мои части fetch_and_xxxx были неправильными. Исправлено это и удалены части API, в которых я не уверен (шаблонные модели памяти). И реализовал другие операции в терминах оператора + =, чтобы избежать повторения кода.
Добавлен: Добавлен оператор * = и оператор / =, поскольку без них числа с плавающей запятой не были бы числами с плавающей запятой. Благодаря комментарию Петерхена, это было замечено
Редактировать: Далее следует последняя версия кода (я оставлю старую версию для справки)
#include <tbb/atomic.h>
typedef unsigned int uint_32;
typedef __TBB_LONG_LONG uint_64;
template<typename FLOATING_POINT,typename MEMORY_BLOCK>
struct atomic_float_
{
/* CRC Card -----------------------------------------------------
| Class: atmomic float template class
|
| Responsability: handle integral atomic memory as it were a float,
| but partially bypassing FPU, SSE/MMX, so it is
| slower than a true float, but faster and smaller
| than a locked float.
| *Warning* If your float usage is thwarted by
| the A-B-A problem this class isn't for you
| *Warning* Atomic specification says we return,
| values not l-values. So (i = j) = k doesn't work.
|
| Collaborators: intel's tbb::atomic handles memory atomicity
----------------------------------------------------------------*/
typedef typename atomic_float_<FLOATING_POINT,MEMORY_BLOCK> self_t;
tbb::atomic<MEMORY_BLOCK> atomic_value_;
template<memory_semantics M>
FLOATING_POINT fetch_and_store( FLOATING_POINT value )
{
const MEMORY_BLOCK value_ =
atomic_value_.tbb::atomic<MEMORY_BLOCK>::fetch_and_store<M>((MEMORY_BLOCK&)value);
//atomic specification requires returning old value, not new one
return reinterpret_cast<const FLOATING_POINT&>(value_);
}
FLOATING_POINT fetch_and_store( FLOATING_POINT value )
{
const MEMORY_BLOCK value_ =
atomic_value_.tbb::atomic<MEMORY_BLOCK>::fetch_and_store((MEMORY_BLOCK&)value);
//atomic specification requires returning old value, not new one
return reinterpret_cast<const FLOATING_POINT&>(value_);
}
template<memory_semantics M>
FLOATING_POINT compare_and_swap( FLOATING_POINT value, FLOATING_POINT comparand )
{
const MEMORY_BLOCK value_ =
atomic_value_.tbb::atomic<MEMORY_BLOCK>::compare_and_swap<M>((MEMORY_BLOCK&)value,(MEMORY_BLOCK&)compare);
//atomic specification requires returning old value, not new one
return reinterpret_cast<const FLOATING_POINT&>(value_);
}
FLOATING_POINT compare_and_swap(FLOATING_POINT value, FLOATING_POINT compare)
{
const MEMORY_BLOCK value_ =
atomic_value_.tbb::atomic<MEMORY_BLOCK>::compare_and_swap((MEMORY_BLOCK&)value,(MEMORY_BLOCK&)compare);
//atomic specification requires returning old value, not new one
return reinterpret_cast<const FLOATING_POINT&>(value_);
}
operator FLOATING_POINT() const volatile // volatile qualifier here for backwards compatibility
{
const MEMORY_BLOCK value_ = atomic_value_;
return reinterpret_cast<const FLOATING_POINT&>(value_);
}
//Note: atomic specification says we return the a copy of the base value not an l-value
FLOATING_POINT operator=(FLOATING_POINT rhs)
{
const MEMORY_BLOCK value_ = atomic_value_.tbb::atomic<MEMORY_BLOCK>::operator =((MEMORY_BLOCK&)rhs);
return reinterpret_cast<const FLOATING_POINT&>(value_);
}
//Note: atomic specification says we return an l-value when operating among atomics
self_t& operator=(self_t& rhs)
{
const MEMORY_BLOCK value_ = atomic_value_.tbb::atomic<MEMORY_BLOCK>::operator =((MEMORY_BLOCK&)rhs);
return *this;
}
FLOATING_POINT& _internal_reference() const
{
return reinterpret_cast<FLOATING_POINT&>(atomic_value_.tbb::atomic<MEMORY_BLOCK>::_internal_reference());
}
FLOATING_POINT operator+=(FLOATING_POINT value)
{
FLOATING_POINT old_value_, new_value_;
do
{
old_value_ = reinterpret_cast<FLOATING_POINT&>(atomic_value_);
new_value_ = old_value_ + value;
//floating point binary representation is not an issue because
//we are using our self's compare and swap, thus comparing floats and floats
} while(self_t::compare_and_swap(new_value_,old_value_) != old_value_);
return (new_value_); //return resulting value
}
FLOATING_POINT operator*=(FLOATING_POINT value)
{
FLOATING_POINT old_value_, new_value_;
do
{
old_value_ = reinterpret_cast<FLOATING_POINT&>(atomic_value_);
new_value_ = old_value_ * value;
//floating point binary representation is not an issue becaus
//we are using our self's compare and swap, thus comparing floats and floats
} while(self_t::compare_and_swap(new_value_,old_value_) != old_value_);
return (new_value_); //return resulting value
}
FLOATING_POINT operator/=(FLOATING_POINT value)
{
FLOATING_POINT old_value_, new_value_;
do
{
old_value_ = reinterpret_cast<FLOATING_POINT&>(atomic_value_);
new_value_ = old_value_ / value;
//floating point binary representation is not an issue because
//we are using our self's compare and swap, thus comparing floats and floats
} while(self_t::compare_and_swap(new_value_,old_value_) != old_value_);
return (new_value_); //return resulting value
}
FLOATING_POINT operator-=(FLOATING_POINT value)
{
return this->operator+=(-value); //return resulting value
}
//Prefix operator
FLOATING_POINT operator++()
{
return this->operator+=(1); //return resulting value
}
//Prefix operator
FLOATING_POINT operator--()
{
return this->operator+=(-1); //return resulting value
}
//Postfix operator
FLOATING_POINT operator++(int)
{
const FLOATING_POINT temp = this;
this->operator+=(1);
return temp//return resulting value
}
//Postfix operator
FLOATING_POINT operator--(int)
{
const FLOATING_POINT temp = this;
this->operator+=(1);
return temp//return resulting value
}
FLOATING_POINT fetch_and_add( FLOATING_POINT addend )
{
const FLOATING_POINT old_value_ = atomic_value_;
this->operator+=(addend);
//atomic specification requires returning old value, not new one as in operator x=
return old_value_;
}
FLOATING_POINT fetch_and_increment()
{
const FLOATING_POINT old_value_ = atomic_value_;
this->operator+=(+1);
//atomic specification requires returning old value, not new one as in operator x=
return old_value_;
}
FLOATING_POINT fetch_and_decrement()
{
const FLOATING_POINT old_value_ = atomic_value_;
this->operator+=(-1);
//atomic specification requires returning old value, not new one as in operator x=
return old_value_;
}
};
typedef atomic_float_<float,uint_32> AtomicFloat;
typedef atomic_float_<double,uint_64> AtomicDouble;
Хороший момент, и он рассмотрен в моей последней версии кода в ответах. Однако возвращение T, а не lvalue для operator = является правильным поведением для атомарных значений в tbb.
привет @RobertGould. Большое спасибо за реализацию этой функции и за то, что поделились ею здесь. У меня два вопроса: (1) время еще в силе? Я имею в виду, что на моей платформе я не могу получить ускорение, когда использую атомарную версию в отличие от std::mutex, (2) есть ли какая-то лицензия для этого фрагмента кода? Что мне делать, если я одолжу его и заставлю работать с std::atomic в стандартной библиотеке моего проекта?





Похоже, ваша реализация предполагает, что sizeof(size_t) == sizeof(float). Всегда ли это будет верно для ваших целевых платформ?
И я бы не стал называть ересью заправка столько, сколько ересью Кастинг. :)
Не обязательно, но я планирую поставить статическое утверждение, которое сравнивает sizeof (float) == sizeof (size_t) в качестве защиты для компиляции.
Что это дает вам от простого использования uint32_t?
Похоже, ваша реализация предполагает, что sizeof (uint32_t) == sizeof (float). Всегда ли это будет верно для ваших целевых платформ? Всегда ли это будет верно для ваших компиляторов?
Вероятно, этого достаточно для его текущей платформы, если есть какие-то будущие платформы, но использование статического утверждения даст ему знать, когда это не так, и если он хочет стать действительно умным, он, вероятно, сможет делать разные определения MACRO для разных платформы.
Я бы серьезно посоветовал отказаться от публичного наследования. Я не знаю, на что похожа атомарная реализация, но я предполагаю, что у нее есть перегруженные операторы, которые используют ее как интегральный тип, что означает, что эти рекламные акции будут использоваться вместо вашего float во многих (может быть, в большинстве?) Случаев.
Я не вижу причин, по которым это не сработает, но, как и вы, я должен доказать это ...
Одно замечание: ваша процедура operator float() не имеет семантики загрузки-получения, и не следует ли ее пометить как const volatile (или определенно как минимум const)?
Обновлено: если вы собираетесь предоставить оператор - (), вы должны предоставить обе формы префикса / постфикса.
Композиция, вероятно, лучшее решение. Мне, вероятно, следует провести рефакторинг класса, если реализация в порядке.
Полностью согласен с наследством - составом.
Прочитав этот код, я бы очень рассердился на такой компилятор, который выполнил бы сборку, не являющуюся атомарной.
Пусть ваш компилятор сгенерирует код сборки и взглянет на него. Если операция представляет собой более чем одну инструкцию на языке ассемблера, тогда это нет атомарная операция, требующая блокировок для правильной работы в многопроцессорных системах.
К сожалению, я не уверен, что верно и обратное - операции являются с одной инструкцией гарантированно будут атомарными. Я не знаю деталей многопроцессорного программирования до этого уровня. Я мог бы привести аргументы в пользу любого результата. (Если у кого-то еще есть определенная информация по этому поводу, не стесняйтесь вмешиваться.)
Отдельные инструкции ASM следует считать неатомарными, пока не будет доказано обратное, особенно на x86 и других архитектурах CISCy, поскольку инструкция разбита на микрооперации, между которыми может быть переключение контекста. Атомарные insns, такие как сравнение и замена, отключают прерывания, чтобы исключить это.
Инструкции на одном языке ассемблера не являются атомарными в многопроцессорных системах независимо от того, выполняет ли какой-либо из процессоров переключение контекста. Способ достичь атомарности - использовать операции, специально разработанные для этого, такие как сравнение и замена, или блокировка, или алгоритм Деккера.
Конечно, в многопроцессорной системе переключение контекста само по себе не имеет значения, но тот факт, что вы должны исследовать каждое возможное чередование выполнения потоков, не меняет того, произвольно ли несколько потоков мультиплексируются по времени на ядро или мультиплексируются по времени в совместно используемые. объем памяти.
Это состояние кода, в том виде, в каком он есть сейчас после переговоров на досках Intel, но до сих пор не было тщательно проверено для правильной работы во всех сценариях.
#include <tbb/atomic.h>
typedef unsigned int uint_32;
typedef __TBB_LONG_LONG uint_64;
template<typename FLOATING_POINT,typename MEMORY_BLOCK>
struct atomic_float_
{
/* CRC Card -----------------------------------------------------
| Class: atmomic float template class
|
| Responsability: handle integral atomic memory as it were a float,
| but partially bypassing FPU, SSE/MMX, so it is
| slower than a true float, but faster and smaller
| than a locked float.
| *Warning* If your float usage is thwarted by
| the A-B-A problem this class isn't for you
| *Warning* Atomic specification says we return,
| values not l-values. So (i = j) = k doesn't work.
|
| Collaborators: intel's tbb::atomic handles memory atomicity
----------------------------------------------------------------*/
typedef typename atomic_float_<FLOATING_POINT,MEMORY_BLOCK> self_t;
tbb::atomic<MEMORY_BLOCK> atomic_value_;
template<memory_semantics M>
FLOATING_POINT fetch_and_store( FLOATING_POINT value )
{
const MEMORY_BLOCK value_ =
atomic_value_.tbb::atomic<MEMORY_BLOCK>::fetch_and_store<M>((MEMORY_BLOCK&)value);
//atomic specification requires returning old value, not new one
return reinterpret_cast<const FLOATING_POINT&>(value_);
}
FLOATING_POINT fetch_and_store( FLOATING_POINT value )
{
const MEMORY_BLOCK value_ =
atomic_value_.tbb::atomic<MEMORY_BLOCK>::fetch_and_store((MEMORY_BLOCK&)value);
//atomic specification requires returning old value, not new one
return reinterpret_cast<const FLOATING_POINT&>(value_);
}
template<memory_semantics M>
FLOATING_POINT compare_and_swap( FLOATING_POINT value, FLOATING_POINT comparand )
{
const MEMORY_BLOCK value_ =
atomic_value_.tbb::atomic<MEMORY_BLOCK>::compare_and_swap<M>((MEMORY_BLOCK&)value,(MEMORY_BLOCK&)compare);
//atomic specification requires returning old value, not new one
return reinterpret_cast<const FLOATING_POINT&>(value_);
}
FLOATING_POINT compare_and_swap(FLOATING_POINT value, FLOATING_POINT compare)
{
const MEMORY_BLOCK value_ =
atomic_value_.tbb::atomic<MEMORY_BLOCK>::compare_and_swap((MEMORY_BLOCK&)value,(MEMORY_BLOCK&)compare);
//atomic specification requires returning old value, not new one
return reinterpret_cast<const FLOATING_POINT&>(value_);
}
operator FLOATING_POINT() const volatile // volatile qualifier here for backwards compatibility
{
const MEMORY_BLOCK value_ = atomic_value_;
return reinterpret_cast<const FLOATING_POINT&>(value_);
}
//Note: atomic specification says we return the a copy of the base value not an l-value
FLOATING_POINT operator=(FLOATING_POINT rhs)
{
const MEMORY_BLOCK value_ = atomic_value_.tbb::atomic<MEMORY_BLOCK>::operator =((MEMORY_BLOCK&)rhs);
return reinterpret_cast<const FLOATING_POINT&>(value_);
}
//Note: atomic specification says we return an l-value when operating among atomics
self_t& operator=(self_t& rhs)
{
const MEMORY_BLOCK value_ = atomic_value_.tbb::atomic<MEMORY_BLOCK>::operator =((MEMORY_BLOCK&)rhs);
return *this;
}
FLOATING_POINT& _internal_reference() const
{
return reinterpret_cast<FLOATING_POINT&>(atomic_value_.tbb::atomic<MEMORY_BLOCK>::_internal_reference());
}
FLOATING_POINT operator+=(FLOATING_POINT value)
{
FLOATING_POINT old_value_, new_value_;
do
{
old_value_ = reinterpret_cast<FLOATING_POINT&>(atomic_value_);
new_value_ = old_value_ + value;
//floating point binary representation is not an issue because
//we are using our self's compare and swap, thus comparing floats and floats
} while(self_t::compare_and_swap(new_value_,old_value_) != old_value_);
return (new_value_); //return resulting value
}
FLOATING_POINT operator*=(FLOATING_POINT value)
{
FLOATING_POINT old_value_, new_value_;
do
{
old_value_ = reinterpret_cast<FLOATING_POINT&>(atomic_value_);
new_value_ = old_value_ * value;
//floating point binary representation is not an issue becaus
//we are using our self's compare and swap, thus comparing floats and floats
} while(self_t::compare_and_swap(new_value_,old_value_) != old_value_);
return (new_value_); //return resulting value
}
FLOATING_POINT operator/=(FLOATING_POINT value)
{
FLOATING_POINT old_value_, new_value_;
do
{
old_value_ = reinterpret_cast<FLOATING_POINT&>(atomic_value_);
new_value_ = old_value_ / value;
//floating point binary representation is not an issue because
//we are using our self's compare and swap, thus comparing floats and floats
} while(self_t::compare_and_swap(new_value_,old_value_) != old_value_);
return (new_value_); //return resulting value
}
FLOATING_POINT operator-=(FLOATING_POINT value)
{
return this->operator+=(-value); //return resulting value
}
//Prefix operator
FLOATING_POINT operator++()
{
return this->operator+=(1); //return resulting value
}
//Prefix operator
FLOATING_POINT operator--()
{
return this->operator+=(-1); //return resulting value
}
//Postfix operator
FLOATING_POINT operator++(int)
{
const FLOATING_POINT temp = this;
this->operator+=(1);
return temp//return resulting value
}
//Postfix operator
FLOATING_POINT operator--(int)
{
const FLOATING_POINT temp = this;
this->operator+=(1);
return temp//return resulting value
}
FLOATING_POINT fetch_and_add( FLOATING_POINT addend )
{
const FLOATING_POINT old_value_ = atomic_value_;
this->operator+=(addend);
//atomic specification requires returning old value, not new one as in operator x=
return old_value_;
}
FLOATING_POINT fetch_and_increment()
{
const FLOATING_POINT old_value_ = atomic_value_;
this->operator+=(+1);
//atomic specification requires returning old value, not new one as in operator x=
return old_value_;
}
FLOATING_POINT fetch_and_decrement()
{
const FLOATING_POINT old_value_ = atomic_value_;
this->operator+=(-1);
//atomic specification requires returning old value, not new one as in operator x=
return old_value_;
}
};
typedef atomic_float_<float,uint_32> AtomicFloat;
typedef atomic_float_<double,uint_64> AtomicDouble;
Хотя размер uint32_t может быть эквивалентен размеру плавать на данной арке, переинтерпретируя приведение от одного к другому, вы неявно предполагаете, что атомарные приращения, декременты и все другие операции с битами семантически эквивалентны для обоих типов , которых на самом деле нет. Я сомневаюсь, что это работает так, как ожидалось.
Нет, нет, поэтому я вытаскиваю фактические операции в цикл транзакции во время выполнения (известный параллельный шаблон). В любом случае я могу заверить вас, что код работает правильно в одном потоке. И даже в многопоточности он работал корректно. Просто не уверен, могу ли я этому доверять ...
На операторов особо не обращал внимания. Но вопрос в том, уверены ли вы, что fetch_and_add, fetch_and_increment и т. д. Работают правильно?
Ты прав! На самом деле я не особо задумывался о них с тех пор, как тестировал операторов. Все fetch_xxxx неверны! глупо я это упустил, им нужно такое же обращение с операторами.
Я сильно сомневаюсь, что вы получите правильные значения в fetch_and_add и т. д., Поскольку добавление float отличается от добавления int.
Вот что я получаю из этой арифметики:
1 + 1 = 1.70141e+038
100 + 1 = -1.46937e-037
100 + 0.01 = 1.56743e+038
23 + 42 = -1.31655e-036
Так что да, потокобезопасный, но не то, что вы ожидаете.
алгоритмы без блокировки (оператор + и т. д.) должны работать в отношении атомарности (не проверяли сам алгоритм ..)
Другое решение: Поскольку это все добавления и вычитания, вы можете дать каждому потоку свой собственный экземпляр, а затем добавить результаты из нескольких потоков.
Заметьте, я этого не делаю. Я преобразую целые числа в ссылки с плавающей запятой, что означает, что они обрабатываются правильно. old_value_ = reinterpret_cast <float &> (* это); новое_значение_ = старое_значение_ + значение;
Это было бы хорошо для решения для «сокращения», но мне нужны числа с плавающей запятой как члены структур данных (свойств), которые имеют долгий срок службы. Но ваш комментарий действительно напоминает мне, что числа с плавающей запятой без умножения и деления глупы. Я тоже добавлю их
Обновленный код выглядит намного лучше! :) И да, мне кажется, что петли без блокировки выглядят нормально, но я не сделал достаточно с ними, чтобы судить.
Просто примечание об этом (я хотел сделать комментарий, но, очевидно, новым пользователям не разрешено комментировать): использование reinterpret_cast в ссылках приводит к неправильному коду с gcc 4.1 -O3. Кажется, это исправлено в 4.4, потому что там это работает. Изменение reinterpret_casts на указатели, хотя и немного уродливее, работает в обоих случаях.
Для оператора = немного неприятно возвращать значение, поскольку для встроенных типов он оценивается как lvalue (T &, где T - это тип). Для этих типов "(i = j) = k" необычно, но допустимо и присваивает значение k i.