0
votes

Is it possible to use atomic operations, possibly using the std::atomic library, when assigning values in a contiguous block of memory.

If I have this code:

uint16_t* data = (uint16_t*) calloc(num_values, size);

What can I do to make operations like this atomic:

data[i] = 5;

I will have multiple threads assigning to data, possibly at the same index, at the same time. The order in which these threads modify the value at a particular index doesn't matter to me, as long as the modifications are atomic, avoiding any possible mangling of the data.

EDIT: So, per @user4581301, I'm providing some context for my issue here. I am writing a program to align depth video data frames to color video data frames. The camera sensors for depth and color have different focal characteristics so they do not come completely aligned. The general algorithm involves projecting a pixel in depth space to a region in color space, then, overwriting all values in the depth frame, spanning that region, with that single pixel. I am parallelizing this algorithm. These projected regions may overlap, thus when paralellized, writes to an index may occur concurrently.

Pseudo-code looks like this:

for x in depth_video_width:
  for y in depth_video_height:
      pixel = get_pixel(x, y)
      x_min, x_max, y_min, y_max = project_depth_pixel(x, y)

      // iterate over projected region
      for x` in [x_min, x_max]:
         for y` in [y_min, y_max]:
             // possible concurrent modification here
             data[x`, y`] = pixel

The outer loop or outermost two loops are parallelized.

2
If you don't actually care what values go into the array then just scribble them in there. I don't see how that would work, but seems to be what you said. If you worry about torn writes where some bytes are set and some aren't, that only matters if the values are different. But if you're writing weirdly different values into the same array elements, I don't see how this plan of yours could ever work. - Zan Lynx
uint16_t* data = (uint16_t*) calloc(num_values, size); might translate into std::vector<uint16_t> data(num_values) if size isn't a surprising value. - user4581301
@Zan Lynx So the values will be weirdly different, but they do have meaning. If there are torn writes, the data will no longer have any meaning. - Matthew Ha
You could have an X-Y problem. Sometimes it's better to step back and ask about the problem this is supposed to solve. - user4581301
Ah, I haven't heard of that term, but it may definitely apply to my case. I'll update my question, thank you. - Matthew Ha

2 Answers

3
votes

You're not going to be able to do exactly what you want like this.

An atomic array doesn't make much sense, nor is it what you want (you want individual writes to be atomic).

You can have an array of atomics:

#include <atomic>
#include <array>

int main()
{
    std::array<std::atomic<uint16_t>, 5> data{};
    data[1] = 5;
}

… but now you can't just access a contiguous block of uint16_ts, which it's implied you want to do.

If you don't mind something platform-specific, you can keep your array of uint16_ts and ensure that you only use atomic operations with each one (e.g. GCC's __atomic intrinsics).

But, generally, I think you're going to want to keep it simple and just lock a mutex around accesses to a normal array. Measure to be sure, but you may be surprised at how much of a performance loss you don't get.

If you're desperate for atomics, and desperate for an underlying array of uint16_t, and desperate for a standard solution, you could wait for C++20 and keep an std::atomic_ref (this is like a non-owning std::atomic) for each element, then access the elements through those. But then you still have to be cautious about any operation accessing the elements directly, possibly by using a lock, or at least by being very careful about what's doing what and when. At this point your code is much more complex: be sure it's worthwhile.

1
votes

To add on the last answer, I would strongly advocate against using an array of atomics since any read or write to an atomic locks an entire cache line (at least on x86). In practice, it means that when accessing element i in your array (either to read or to write it), you would lock the cache line around that element (so other threads couldn't access that particular cache line).

The solution to your problem is a mutex as mentioned in the other answer.

For the maximum supported atomic operations it seems to be currently 64bits (see https://www.intel.com/content/www/us/en/architecture-and-technology/64-ia-32-architectures-software-developer-vol-3a-part-1-manual.html)

The Intel-64 memory ordering model guarantees that, for each of the following 
memory-access instructions, the constituent memory operation appears to execute 
as a single memory access:

• Instructions that read or write a single byte.
• Instructions that read or write a word (2 bytes) whose address is aligned on a 2
byte boundary.
• Instructions that read or write a doubleword (4 bytes) whose address is aligned
on a 4 byte boundary.
• Instructions that read or write a quadword (8 bytes) whose address is aligned on
an 8 byte boundary.

Any locked instruction (either the XCHG instruction or another read-modify-write
 instruction with a LOCK prefix) appears to execute as an indivisible and 
uninterruptible sequence of load(s) followed by store(s) regardless of alignment.

In other word, your processor doesn't know how to do more than 64bits atomic operations. And I'm not even mentioning here the STL implementation of atomic which can use lock (see https://en.cppreference.com/w/cpp/atomic/atomic/is_lock_free).