<p>Yes, on x86 aligned loads are atomic, <strong>BUT</strong> this is an architectural detail that you should <strong>NOT</strong> rely on!</p>

<p>Since you are writing C++ code, you have to abide by the rules of the C++ standard, i.e., you have to use atomics instead of volatile. The fact
that <code>volatile</code> has been part of that language long before the introduction
of threads in C++11 should be a strong enough indication that <code>volatile</code> was
never designed or intended to be used for multi-threading. It is important to
note that in C++ <code>volatile</code> is something fundamentally different from <code>volatile</code>
in languages like Java or C# (in these languages <code>volatile</code> is in
fact related to the memory model and therefore much more like an atomic in C++).</p>

<p>In C++, <code>volatile</code> is used for what is often referred to as "unusual memory".
This is typically memory that can be read or modified outside the current process,
for example when using memory mapped I/O. <code>volatile</code> forces the compiler to
<em>execute all operations in the exact order as specified</em>. This prevents
some optimizations that would be perfectly legal for atomics, while also allowing
some optimizations that are actually <em>illegal</em> for atomics. For example:</p>

<pre><code>volatile int x;
         int y;
volatile int z;

x = 1;
y = 2;
z = 3;
z = 4;

...

int a = x;
int b = x;
int c = y;
int d = z;
</code></pre>

<p>In this example, there are two assignments to <code>z</code>, and two read operations on <code>x</code>.
If <code>x</code> and <code>z</code> were atomics instead of volatile, the compiler would be free to treat
the first store as irrelevant and simply remove it. Likewise it could just reuse the
value returned by the first load of <code>x</code>, effectively generating code like <code>int b = a</code>.
But since <code>x</code> and <code>z</code> are volatile, these optimizations are not possible. Instead,
the compiler has to ensure that <em>all</em> volatile operations are executed in the
<em>exact order as specified</em>, i.e., the volatile operations cannot be reordered with
respect to each other. However, this does not prevent the compiler from reordering
non-volatile operations. For example, the operations on <code>y</code> could freely be moved
up or down - something that would not be possible if <code>x</code> and <code>z</code> were atomics. So
if you were to try implementing a lock based on a volatile variable, the compiler
could simply (and legally) move some code outside your critical section.</p>

<p>Last but not least it should be noted that marking a variable as <code>volatile</code> does
not prevent it from participating in a data race. In those rare cases where you
have some "unusual memory" (and therefore really require <code>volatile</code>) that is
also accessed by multiple threads, you have to use volatile atomics.</p>

<p>Since aligned loads are actually atomic on x86, the compiler will translate an <code>atomic.load()</code> call to a simple <code>mov</code> instruction, so an atomic load is not slower than reading a volatile variable. An <code>atomic.store()</code> is actually slower than writing a volatile variable, but for good reasons, since in contrast to the volatile write it is by default <em>sequentially consistent</em>. You can relax the memory orders, but you <em>really</em> have to know what you are doing!!</p>

<p>If you want to learn more about the C++ memory model, I can recommend this paper: <a href="https://arxiv.org/abs/1803.04432" rel="nofollow noreferrer">Memory Models for C/C++ Programmers</a></p>
C++11 on modern Intel: am I crazy or are non-atomic aligned 64-bit load/store actually atomic?
<p>Can I base a mission-critical application on the results of this test, that 100 threads reading a pointer set a billion times by a main thread never see a tear?</p>

<p>Any other potential problems doing this besides tearing?</p>

<p>Here's a stand-alone demo that compiles with <code>g++ -g tear.cxx -o tear -pthread</code>.</p>

<pre><code>#include &lt;atomic&gt;
#include &lt;thread&gt;
#include &lt;vector&gt;

using namespace std;

void* pvTearTest;
atomic&lt;int&gt; iTears( 0 );

void TearTest( void ) {

  while (1) {
      void* pv = (void*) pvTearTest;

      intptr_t i = (intptr_t) pv;

      if ( ( i &gt;&gt; 32 ) != ( i &amp; 0xFFFFFFFF ) ) {
          printf( "tear: pv = %p\n", pv );
          iTears++;
      }
      if ( ( i &gt;&gt; 32 ) == 999999999 )
          break;

  }
}



int main( int argc, char** argv ) {

  printf( "\n\nTEAR TEST: are normal pointer read/writes atomic?\n" );

  vector&lt;thread&gt; athr;

  // Create lots of threads and have them do the test simultaneously.

  for ( int i = 0; i &lt; 100; i++ )
      athr.emplace_back( TearTest );

  for ( int i = 0; i &lt; 1000000000; i++ )
      pvTearTest = (void*) (intptr_t)
                   ( ( i % (1L&lt;&lt;32) ) * 0x100000001 );

  for ( auto&amp; thr: athr )
      thr.join();

  if ( iTears )
      printf( "%d tears\n", iTears.load() );
  else
      printf( "\n\nTEAR TEST: SUCCESS, no tears\n" );
}
</code></pre>

<p>The actual application is a <code>malloc()</code>'ed and sometimes <code>realloc()</code>'d array (size is power of two; realloc doubles storage) that many child threads will absolutely be hammering in a mission-critical but also high-performance-critical way.</p>

<p>From time to time a thread will need to add a new entry to the array, and will do so by setting the next array entry to point to something, then increment an <code>atomic&lt;int&gt; iCount</code>.  Finally it will add data to some data structures that would cause other threads to attempt to dereference that cell.</p>

<p>It all seems fine (except I'm not positive if the increment of count is assured of happening before following non-atomic updates)... <em>except</em> for one thing: <code>realloc()</code> will typically change the address of the array, and <em>further</em> frees the old one, the pointer to which is still visible to other threads.</p>

<p>OK, so instead of <code>realloc()</code>, I <code>malloc()</code> a new array, manually copy the contents, set the pointer to the array.  I would free the old array but I realize other threads may still be accessing it: they read the array base; I free the base; a third thread allocates it writes something else there; the first thread then adds the indexed offset to the base and expects a valid pointer.  I'm happy to leak those though.  (Given the doubling growth, all old arrays combined are about the same size as the current array so overhead is simply an extra 16 bytes per item, and it's memory that soon is never referenced again.)</p>

<p>So, here's the crux of the question: once I allocate the bigger array, can I write it's base address with a non-atomic write, in utter safety?  Or despite my billion-access test, do I actually have to make it atomic&lt;> and thus slow all worker threads to read that atomic?</p>

<p>(As this is surely environment dependent, we're talking 2012-or-later Intel, g++ 4 to 9, and Red Hat of 2012 or later.)</p>

<p>EDIT: here is a modified test program that matches my planned scenario much more closely, with only a small number of writes.  I've also added a count of the reads.  I see when switching from void* to atomic I go from 2240 reads/sec to 660 reads/sec (with optimization disabled).  The machine language for the read is shown after the source.</p>

<pre><code>#include &lt;atomic&gt;
#include &lt;chrono&gt;
#include &lt;thread&gt;
#include &lt;vector&gt;

using namespace std;

chrono::time_point&lt;chrono::high_resolution_clock&gt; tp1, tp2;

// void*: 1169.093u 0.027s 2:26.75 796.6% 0+0k 0+0io 0pf+0w
// atomic&lt;void*&gt;: 6656.864u 0.348s 13:56.18 796.1%        0+0k 0+0io 0pf+0w

// Different definitions of the target variable.
atomic&lt;void*&gt; pvTearTest;
//void* pvTearTest;

// Children sum the tears they find, and at end, total checks performed.
atomic&lt;int&gt; iTears( 0 );
atomic&lt;uint64_t&gt; iReads( 0 );

bool bEnd = false; // main thr sets true; children all finish.

void TearTest( void ) {

  uint64_t i;
  for ( i = 0; ! bEnd; i++ ) {

      intptr_t iTearTest = (intptr_t) (void*) pvTearTest;

      // Make sure top 4 and bottom 4 bytes are the same.  If not it's a tear.
      if ( ( iTearTest &gt;&gt; 32 ) != ( iTearTest &amp; 0xFFFFFFFF ) ) {
          printf( "tear: pv = %ux\n", iTearTest );
          iTears++;
      }

      // Output periodically to prove we're seeing changing values.
      if ( ( (i+1) % 50000000 ) == 0 )
          printf( "got: pv = %lx\n", iTearTest );
  }

  iReads += i;
}



int main( int argc, char** argv ) {

  printf( "\n\nTEAR TEST: are normal pointer read/writes atomic?\n" );

  vector&lt;thread&gt; athr;

  // Create lots of threads and have them do the test simultaneously.

  for ( int i = 0; i &lt; 100; i++ )
      athr.emplace_back( TearTest );

  tp1 = chrono::high_resolution_clock::now();

#if 0
  // Change target as fast as possible for fixed number of updates.
  for ( int i = 0; i &lt; 1000000000; i++ )
      pvTearTest = (void*) (intptr_t)
                   ( ( i % (1L&lt;&lt;32) ) * 0x100000001 );
#else
  // More like our actual app: change target only periodically, for fixed time.
  for ( int i = 0; i &lt; 100; i++ ) {
      pvTearTest.store( (void*) (intptr_t) ( ( i % (1L&lt;&lt;32) ) * 0x100000001 ),
                        std::memory_order_release );

      this_thread::sleep_for(10ms);
  }
#endif

  bEnd = true;

  for ( auto&amp; thr: athr )
      thr.join();

  tp2 = chrono::high_resolution_clock::now();

  chrono::duration&lt;double&gt; dur = tp2 - tp1;
  printf( "%ld reads in %.4f secs: %.2f reads/usec\n",
          iReads.load(), dur.count(), iReads.load() / dur.count() / 1000000 );

  if ( iTears )
      printf( "%d tears\n", iTears.load() );
  else
      printf( "\n\nTEAR TEST: SUCCESS, no tears\n" );
}
</code></pre>

<p></p>

<pre><code>Dump of assembler code for function TearTest():
   0x0000000000401256 &lt;+0&gt;:     push   %rbp
   0x0000000000401257 &lt;+1&gt;:     mov    %rsp,%rbp
   0x000000000040125a &lt;+4&gt;:     sub    $0x10,%rsp
   0x000000000040125e &lt;+8&gt;:     movq   $0x0,-0x8(%rbp)
   0x0000000000401266 &lt;+16&gt;:    movzbl 0x6e83(%rip),%eax        # 0x4080f0 &lt;bEnd&gt;
   0x000000000040126d &lt;+23&gt;:    test   %al,%al
   0x000000000040126f &lt;+25&gt;:    jne    0x40130c &lt;TearTest()+182&gt;
=&gt; 0x0000000000401275 &lt;+31&gt;:    mov    $0x4080d8,%edi
   0x000000000040127a &lt;+36&gt;:    callq  0x40193a &lt;std::atomic&lt;void*&gt;::operator void*() const&gt;
   0x000000000040127f &lt;+41&gt;:    mov    %rax,-0x10(%rbp)
   0x0000000000401283 &lt;+45&gt;:    mov    -0x10(%rbp),%rax
   0x0000000000401287 &lt;+49&gt;:    sar    $0x20,%rax
   0x000000000040128b &lt;+53&gt;:    mov    -0x10(%rbp),%rdx
   0x000000000040128f &lt;+57&gt;:    mov    %edx,%edx
   0x0000000000401291 &lt;+59&gt;:    cmp    %rdx,%rax
   0x0000000000401294 &lt;+62&gt;:    je     0x4012bb &lt;TearTest()+101&gt;
   0x0000000000401296 &lt;+64&gt;:    mov    -0x10(%rbp),%rax
   0x000000000040129a &lt;+68&gt;:    mov    %rax,%rsi
   0x000000000040129d &lt;+71&gt;:    mov    $0x40401a,%edi
   0x00000000004012a2 &lt;+76&gt;:    mov    $0x0,%eax
   0x00000000004012a7 &lt;+81&gt;:    callq  0x401040 &lt;printf@plt&gt;
   0x00000000004012ac &lt;+86&gt;:    mov    $0x0,%esi
   0x00000000004012b1 &lt;+91&gt;:    mov    $0x4080e0,%edi
   0x00000000004012b6 &lt;+96&gt;:    callq  0x401954 &lt;std::__atomic_base&lt;int&gt;::operator++(int)&gt;
   0x00000000004012bb &lt;+101&gt;:   mov    -0x8(%rbp),%rax
   0x00000000004012bf &lt;+105&gt;:   lea    0x1(%rax),%rcx
   0x00000000004012c3 &lt;+109&gt;:   movabs $0xabcc77118461cefd,%rdx
   0x00000000004012cd &lt;+119&gt;:   mov    %rcx,%rax
   0x00000000004012d0 &lt;+122&gt;:   mul    %rdx
   0x00000000004012d3 &lt;+125&gt;:   mov    %rdx,%rax
   0x00000000004012d6 &lt;+128&gt;:   shr    $0x19,%rax
   0x00000000004012da &lt;+132&gt;:   imul   $0x2faf080,%rax,%rax
   0x00000000004012e1 &lt;+139&gt;:   sub    %rax,%rcx
   0x00000000004012e4 &lt;+142&gt;:   mov    %rcx,%rax
   0x00000000004012e7 &lt;+145&gt;:   test   %rax,%rax
   0x00000000004012ea &lt;+148&gt;:   jne    0x401302 &lt;TearTest()+172&gt;
   0x00000000004012ec &lt;+150&gt;:   mov    -0x10(%rbp),%rax
   0x00000000004012f0 &lt;+154&gt;:   mov    %rax,%rsi
   0x00000000004012f3 &lt;+157&gt;:   mov    $0x40402a,%edi
   0x00000000004012f8 &lt;+162&gt;:   mov    $0x0,%eax
   0x00000000004012fd &lt;+167&gt;:   callq  0x401040 &lt;printf@plt&gt;
   0x0000000000401302 &lt;+172&gt;:   addq   $0x1,-0x8(%rbp)
   0x0000000000401307 &lt;+177&gt;:   jmpq   0x401266 &lt;TearTest()+16&gt;
   0x000000000040130c &lt;+182&gt;:   mov    -0x8(%rbp),%rax
   0x0000000000401310 &lt;+186&gt;:   mov    %rax,%rsi
   0x0000000000401313 &lt;+189&gt;:   mov    $0x4080e8,%edi
   0x0000000000401318 &lt;+194&gt;:   callq  0x401984 &lt;std::__atomic_base&lt;unsigned long&gt;::operator+=(unsigned long)&gt;
   0x000000000040131d &lt;+199&gt;:   nop
   0x000000000040131e &lt;+200&gt;:   leaveq
   0x000000000040131f &lt;+201&gt;:   retq
</code></pre>