Home > Articles > Programming > C/C++

C++ Reference Guide

Hosted by

Toggle Open Guide Table of ContentsGuide Contents

Close Table of ContentsGuide Contents

Close Table of Contents

What Are Atomic Operations?

Last updated Jan 1, 2003.

In multithreaded code, you often need to distinguish between atomic and non-atomic operations. Find out what an atomic operation is, and how you can check whether a C++ statement is indeed atomic.

Atomicity

An atomic operation is a sequence of one or more machine instructions that are executed sequentially, without interruption. By default, any sequence of two or more machine instructions isn't atomic since the operating system may suspend the execution of the current sequence of operations in favor of another task. If you want to ensure that a sequence of operations is atomic you must use some form of locking or other types of synchronization. Without that, the only guarantee you have is that a single machine instruction is always atomic -- the CPU will not interrupt a single instruction in the middle. We can conclude from that minimal guarantee that if you can prove that your compiler translates a certain C++ statement into a single machine instruction, that C++ statement is naturally atomic meaning, the programmer doesn't have to use explicit locking to enforce the atomic execution of that statement.

Which C++ Statements are Naturally Atomic?

Obviously, there are very few universal rules of thumb because each hardware architecture might translate the same C++ statement differently. Many textbooks tell you that the unary ++ and -- operators, when applied to integral types and pointers, are guaranteed to be atomic. Historically, when Dennis Ritchie and Brian Kernighan designed C, they added these operators to the language because they wanted to take advantage of the fast INC (increment) assembly directive that many machines supported. However, there is no guarantee in the C or C++ standards that these operators shall be atomic. Ritchie and Kernighan were more concerned about speed rather than atomicity.

You shouldn't make assumptions about the atomicity of C++ statements without examining the output of your compiler. In some cases, you might discover that what appears to be a single C++ statement is in fact translated into a long and complex set of machine instructions.

Increment and Decrement Operations

Let's look at a few examples, starting with increment and decrement operators applied to integers and pointers:

int x=7, y=0;
long double * p =0;
//which of the statements below are atomic?
p++; 
x++;
++y;
y--;
--p;

Consider the last five lines of code. Each line consists of a single C++ expression that involves a built-in unary operator. The first line of C++ code is translated into the following assembly directives on a typical Win32 platform:

p++; 
add dword ptr [ebp-0x64],0x0a

You don't need to know what this assembly directive means; what matters is that the C++ line is translated into a single line of assembly code.

Note: although they are often used interchangeably, machine code and assembly code aren't the same thing. Assembly languages use mnemonic keywords such as inc and add. In contrast, machine code uses a sequence of binary digits that the CPU understands. In other words, assembly is a human-readable rendition of machine code.

We can thus conclude that applying operator ++ to a pointer is an atomic operation on the said platform. What about incrementing an integer?

x++;
inc dword ptr [ebp-0x58]

There's no surprise here either. An integral post increment operation is also atomic. Notice however that the compiler uses the inc (increment) directive in this case, rather than add that was used for the pointer increment operation.

Would it make a difference if you used pre-increment instead of post increment? No, it wouldn't:

++y;
inc dword ptr [ebp-0x5c]

As an aside, this shows that there's no performance difference between the pre-increment and post increment operations, at least as far as integral types are concerned.

Would it make a difference if you used -- instead of ++? Here's the compiler's verdict:

y--;
dec dword ptr [ebp-0x5c]

The assembly directive has changed again: the dec (decrement) directive is used in this case. However, with respect to atomicity, there is no difference -- decrementing is an atomic operation too.

Assignment

Is it true that integer assignment is also atomic?

Let's disassemble the following C++ statements:

y=y+2;
add dword ptr [ebp-0x64],0x02

Assigning integers is an atomic operation. What about pointer arithmetic?

p+=10; //assigning to a pointer

Since the compiler can calculate the precise offset statically, you can expect that pointer assignment should be atomic, just as integer assignment:

p+=10; //assigning to a pointer
add dword ptr [ebp-0x64],0x64

In summary, incrementing and decrementing of integral types and pointers are atomic. Additionally, assigning pointers and integers are also atomic. For C programmers, these conclusions cover all bases. Now let's look at object-oriented stuff.

Iterators

The Standard Library uses iterators as generic pointers. However, are iterators truly as efficient as pointers are? The C++ Standard doesn't state how iterators are implemented; in most cases they are truly bare pointers disguised as typedefs, but not always. Here again, you have to draw conclusion by analyzing every iterator type carefully. Let's look at a basic iterator example:

vector<int> vi;
vi.push_back(9);
vector<int>::iterator it=vi.begin();
it++; //is it atomic

The compiler's verdict is:

add dword ptr [ebp-0x54],0x04

Incrementing a vector<int>::iterator is in fact identical to incrementing an int*, which gives away the underlying type of the iterator -- a pointer to int.

Thus far, you have seen a single assembly line for every C++ statement. The problem however is that not every increment, decrement or assignment operation uses the built-in operators. What if ++ is an overloaded operator?

Overloaded ++

I deliberately used a buggy implementation for an overloaded ++ in order to minimize the number of assembly directives required. Additionally, I declared the overloaded ++ inline:

struct S
{
int x;
inline int operator++ () { return ++x;} //prefix
};
S s1;

Recall that an overloaded ++ that takes no parameters is the prefix version; the postfix version takes a dummy int parameter.

You would expect that under these circumstances (inline, minimal number of C++ statements in the function's body), the compiler should translate the following C++ statement into a single assembly directive:

s1++;

However, with all optimizations turned off, the compiler generates the following code:

lea eax, [ebp-0x60]
push eax
call S::operator++()
pop ecx

Clearly, the expression s1++; isn't atomic. Rather, it's as if you called a full-blown function. Would it matter if you used the pre-increment version? Not really:

s1++;
//the above is translated into:
lea edx, [ebp-0x60]
push edx
call S::operator++()
pop ecx

There appears to be no difference (apart from the CPU register used) in either case. By the way, don't let the four assembly lines mislead you. The third assembly line actually calls a function (the overloaded operator). If you step into that call, you will discover that function body itself consists of five more assembly directives. In total, a seemingly atomic s1++; expression is translated into a sequence of nine assembly directives!

Epilog

The multithreading support of C++0x consists of a thread class as well as a standard atomics library that guarantees the atomicity of logical and arithmetic operations. I will introduce the C++0x atomics library in a separate column.