I need to be able to atomically add a 32-bit floating point value to a location in memory. Here is what I came up with. While the code is Windows-specific, I'll extend it with Linux support using __sync_bool_compare_and_swap()
.
While I tested this code and it appeared to work fine, I'll appreciate a fresh look from another pair of eyes to make sure this code is 100% safe.
I'm also very much interested in any performance advice as this code is used in a hot code path in a performance-sensitive application (interactive rendering).
atomic_float_add()
, the main function:
__forceinline void atomic_float_add(volatile float* ptr, const float operand)
{
assert(is_aligned(ptr, 4));
volatile LONG* lptr = reinterpret_cast<volatile LONG*>(ptr);
LONG lorg, lnew;
do
{
const float forg = *ptr;
const float fnew = forg + operand;
lorg = binary_cast<LONG>(forg);
lnew = binary_cast<LONG>(fnew);
} while (InterlockedCompareExchange(lptr, lnew, lorg) != lorg);
}
binary_cast()
is implemented in such a way that it obeys strict aliasing rules:
template <typename Target, typename Source>
inline Target binary_cast(Source s)
{
BOOST_STATIC_ASSERT(sizeof(Target) == sizeof(Source));
union
{
Source m_source;
Target m_target;
} u;
u.m_source = s;
return u.m_target;
}
is_aligned()
:
template <typename T>
inline bool is_aligned(const T ptr, const size_t alignment)
{
assert(alignment > 0);
assert(is_pow2(alignment));
const uintptr_t p = (uintptr_t)ptr;
return (p & (alignment - 1)) == 0;
}
is_pow2()
:
template <typename T>
inline bool is_pow2(const T x)
{
return (x & (x - 1)) == 0;
}
Assembly output for atomic_float_add()
(Visual Studio 2013, full optimization):
lorg$ = 8
ptr$ = 8
lnew$ = 16
operand$ = 16
?atomic_float_add@?A0x1534477b@foundation@@YAXPECMM@Z PROC
prefetchw BYTE PTR [rcx]
npad 13
$LL3@atomic_flo:
movss xmm0, DWORD PTR [rcx]
movss DWORD PTR lorg$[rsp], xmm0
addss xmm0, xmm1
mov eax, DWORD PTR lorg$[rsp]
movss DWORD PTR lnew$[rsp], xmm0
mov edx, DWORD PTR lnew$[rsp]
lock cmpxchg DWORD PTR [rcx], edx
jne SHORT $LL3@atomic_flo
ret 0
?atomic_float_add@?A0x1534477b@foundation@@YAXPECMM@Z ENDP
EDIT: here's a bit of context on how this code is used:
As you can see, atomic_float_add()
is taking the bulk of the time of FilteredTile::add()
, which itself is the most expensive function of the whole program in this particular setup, taking nearly 27% of the overall time.
std::atomic<>
? \$\endgroup\$std::atomic
. This is beyond our control. In any case, C++11's and Boost's atomics require a type change which is not acceptable in our context. \$\endgroup\$InterlockedCompareExchange
, something with weaker semantics maybe? \$\endgroup\$InterlockedCompareExchange
returns the current value, you do not have to load fromptr
in the loop if you reverse one of thebinary_cast
s. \$\endgroup\$