Безопасна ли эта реализация C++ для Atomic float?
-
04-07-2019 - |
Вопрос
Редактировать: В приведенном здесь коде все еще есть ошибки, и его можно было бы улучшить с точки зрения производительности, но вместо того, чтобы пытаться это исправить, для протокола я передал проблему в дискуссионные группы Intel и получил множество отличных отзывов, и если все идет хорошо, доработанная версия Atomic float будет включена в ближайший выпуск Intel 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 секунды.Таким образом, более чем 30-кратное повышение производительности означает, что оно того стоит (и в этом загвоздка), если это правильно.
Еще больше правок: Как заметил Аугн, мой 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;
Решение
Я бы серьезно советовал против публичного наследования.Я не знаю, на что похожа атомарная реализация, но я предполагаю, что в ней есть перегруженные операторы, которые используют его как целочисленный тип, а это означает, что эти повышения будут использоваться вместо вашего числа с плавающей запятой во многих (может быть, в большинстве?) случаях.
Я не вижу причин, почему это не сработает, но мне, как и вам, нужно найти способ доказать это...
Одна запись:твой operator float()
подпрограмма не имеет семантики загрузки, и не должна ли она быть помечена как const volutable (или, по крайней мере, как минимум const)?
РЕДАКТИРОВАТЬ:Если вы собираетесь предоставить оператор --(), вам следует предоставить обе формы префикса и постфикса.
Другие советы
Похоже, ваша реализация предполагает, что sizeof(size_t) == sizeof(float)
.Всегда ли это будет справедливо для ваших целевых платформ?
И я бы не сказал резьба ересь настолько, насколько Кастинг ересь.:)
Хотя размер А uint32_t может быть эквивалентно плавать на данной арке, переосмысливая приведение из одного в другой, вы неявно предполагаете, что атомарные приращения, декременты и все другие операции с битами семантически эквивалентны для обоих типов, чего на самом деле нет.Я сомневаюсь, что это работает так, как ожидалось.
Я сильно сомневаюсь, что вы получите правильные значения в fetch_and_add и т. д., поскольку сложение с плавающей запятой отличается от сложения int.
Вот что я получаю из этой арифметики:
1 + 1 = 1.70141e+038
100 + 1 = -1.46937e-037
100 + 0.01 = 1.56743e+038
23 + 42 = -1.31655e-036
Так что да, потокобезопасен, но это не то, что вы ожидаете.
алгоритмы блокировки (оператор + и т.д.) должны работать с точки зрения атомарности (сам алгоритм не проверялся..)
Другое решение:Поскольку это все сложения и вычитания, вы можете предоставить каждому потоку собственный экземпляр, а затем добавить результаты из нескольких потоков.
Это состояние кода в его нынешнем виде после переговоров на форумах 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;
Просто примечание по этому поводу (я хотел оставить комментарий, но, видимо, новым пользователям не разрешено оставлять комментарии):Использование reinterpret_cast для ссылок приводит к неправильному коду с gcc 4.1 -O3.Кажется, это исправлено в 4.4, потому что там это работает.Изменение reinterpret_casts на указатели, хотя и немного уродливое, работает в обоих случаях.
Прочитав этот код, я был бы очень зол на такой компилятор, который бы выпускал для него ассемблер, который не был атомарным.
Пусть ваш компилятор сгенерирует ассемблерный код и просмотрит его.Если операция представляет собой более чем одну инструкцию на языке ассемблера, то она нет является атомарной операцией и требует, чтобы блокировки работали правильно в многопроцессорных системах.
К сожалению, я не уверен, что верно и обратное: операции с одной инструкцией являются гарантированно будет атомарным.Я не знаю подробностей многопроцессорного программирования до этого уровня.Я мог бы обосновать любой результат.(Если у кого-то еще есть точная информация по этому поводу, не стесняйтесь присоединиться.)