{"id":5048,"date":"2026-11-06T17:44:50","date_gmt":"2026-11-07T01:44:50","guid":{"rendered":"https:\/\/www.flamingspork.com\/blog\/?p=5048"},"modified":"2026-11-06T17:47:05","modified_gmt":"2026-11-07T01:47:05","slug":"double-width-atomics-arent-atomic-if-using-zig-cc-to-build-c-c-code","status":"publish","type":"post","link":"https:\/\/www.flamingspork.com\/blog\/2026\/11\/06\/double-width-atomics-arent-atomic-if-using-zig-cc-to-build-c-c-code\/","title":{"rendered":"Double-width atomics aren&#8217;t atomic if using &#8220;zig cc&#8221; to build C\/C++ code"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">The Zig project heavily advocates the use case of using its toolchain to build C\/C++ code, &#8220;Use Zig as a zero-dependency, drop-in C\/C++ compiler that supports cross-compilation out-of-the-box.\u201d (Zig home page <a href=\"https:\/\/ziglang.org\/\">https:\/\/ziglang.org\/<\/a> fetched 2026-10-21). Back in 2020, the announcement from Zig&#8217;s creator was <a href=\"https:\/\/andrewkelley.me\/post\/zig-cc-powerful-drop-in-replacement-gcc-clang.html\">&#8220;zig cc: a Powerful Drop-In Replacement for GCC\/Clang&#8221;<\/a>. As far as I can work out, the vast majority of the usage of the Zig toolchain is as a C\/C++ compiler, and since it is LLVM based, one may reasonably assume that it is pretty much the same as using <code>clang<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Unfortunately, this is not true if you rely on some atomic operations being, well, atomic.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">I think going through the details on this bug is a pretty interesting learning exercise (I certainly learned some things), so here goes&#8230;.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A compiler (at least in the Linux world, I assume it&#8217;s similar on other platforms) implements an atomic operation by emitting a lock-free instruction, or by calling a library function (e.g. <code>__atomic_load_16<\/code>, <code>__atomic_load_8<\/code>, etc) if building for a target which lacks a suitable lock-free instruction. This enables a compiler to emit efficient native code for the target, as well as have a library that can be used in two cases: 1) The base processor being targeted doesn&#8217;t have an instruction for the operation, and 2) the implementation of the library calls can be optimal for the CPU the program is running on, thus a program can target an older CPU but get (most of) the benefit of improved atomic instructions on newer processors.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The lock-free instruction and the implementation of the equivalent library function needs to be compatible as either form can be used on the same object in the same program. This allows building software that works across a large range of processors but has the hot paths built for different micro-architectures and selected at runtime. This is effectively the same thing as supporting linking in objects built with a prior version of the compiler that didn&#8217;t know about the lock-free instruction. Thus, LLVM and GCC require that the library implementing the atomic functions to be lock-free for every size the compiler can emit inline, and both link to GNU <code>libatomic<\/code> for the library functions.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">What the toolchains require<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The <a href=\"https:\/\/llvm.org\/docs\/Atomics.html#libcalls-atomic\">LLVM Atomics guide, &#8220;Libcalls: <code>__atomic_*<\/code>&#8220;<\/a>:<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\">The set of atomic sizes supported lock-free must be a superset of the sizes any compiler can emit. That is: if a new compiler introduces support for inline-lock-free atomics of size N, the <code>__atomic_*<\/code> functions must also have a lock-free implementation for size N. This is a requirement so that code produced by an old compiler (which will have called the <code>__atomic_*<\/code> function) interoperates with code produced by the new compiler (which will use native the atomic instruction).<\/p>\n<\/blockquote>\n\n\n\n<p class=\"wp-block-paragraph\">GCC&#8217;s design for its atomic library, <a href=\"https:\/\/gcc.gnu.org\/wiki\/Atomic\/GCCMM\/LIbrary\">Atomic\/GCCMM\/LIbrary on the GCC wiki<\/a> (<a href=\"https:\/\/web.archive.org\/web\/20241230001222\/https:\/\/gcc.gnu.org\/wiki\/Atomic\/GCCMM\/LIbrary\">archived copy, 2024-12-30<\/a>):<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\">Implementation of the external routines must agree with the compiler for any given object size. If the compiler generates lock-free code for an object, the library must also implement lock free for that size as well. It is conforming for the compiler to simply always call the library, so both must agree on the implementation.<\/p>\n<\/blockquote>\n\n\n\n<p class=\"wp-block-paragraph\">The same page gives the 32-bit test below as its example:<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\">Compile a library for 386. The 386 does not support a lock-free atomic int, so the compiler generates function calls. Compile an application for 486. The 486 does support a lock-free atomic int, so the compiler generates machine instructions. Link those two and run them on a 486. The operating system was built knowing that it&#8217;s architectural level is 486, so its atomic ABI library implements the atomic int with lock-free machine instructions.<\/p>\n<\/blockquote>\n\n\n\n<h2 class=\"wp-block-heading\">The problem with &#8220;zig cc&#8221;<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">However, when you build your C\/C++ code using <code>zig cc<\/code>, it will emit lock-free instructions and calls to library functions, but it <strong>does not link to GNU libatomic<\/strong>, instead it links to Zig&#8217;s bundled <code>compiler_rt<\/code> library, which contains its own implementations.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">In Zig&#8217;s <code>compiler_rt<\/code> library, which <strong>is linked to your C\/C++ code<\/strong>,  <code>__atomic_load_16<\/code> on <code>x86_64<\/code> and <code>ppc64le<\/code>, and <code>__atomic_load_8<\/code> on 32-bit <code>x86<\/code>, are implemented by taking a spinlock and then doing two separate half-size loads. This obviously breaks if the compiler can emit a lock-free instruction for these operations, <strong>which it does.<\/strong> Thus, loads tear and your atomic operations are not atomic. While it is possible to pass <code>-latomic<\/code> to <code>zig cc<\/code>, this makes no difference.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Reproducing with a single C file (x86-64, 16-byte atomics)<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">We will use a small C program where a writer thread and a reader thread access one <code>_Atomic unsigned __int128<\/code>. We use the <code>__attribute__((target(\"cx16\")))<\/code> feature of the compiler as a shortcut for situations where you&#8217;d have different implementations of one function optimized for different CPUs or for linking in different builds.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">If we build the source with the default options, since <code>writer()<\/code> has <code>__attribute__((target(\"cx16\")))<\/code>, its <code>atomic_store<\/code> will be an inline <code>lock cmpxchg16b<\/code>, and since <code>reader()<\/code> has no attribute, its <code>atomic_load<\/code> is a call to <code>__atomic_load_16<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">(spoiler) if you use clang or gcc to compile and link, this will work perfectly.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">here&#8217;s the code:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/*\n * Test: the writer only stores values whose two 8-byte halves are equal;\n *       a load with unequal halves is torn, and should NEVER happen.\n *\/\n#include &lt;stdatomic.h>\n#include &lt;stdio.h>\n#include &lt;pthread.h>\n#include &lt;time.h>\n\nstatic _Atomic unsigned __int128 shared;\nstatic atomic_int stop;\n\n__attribute__((target(\"cx16\"))) static void *writer(void *arg) {\n    (void)arg;\n    for (unsigned long long i = 1; !atomic_load(&amp;stop); i++)\n        atomic_store(&amp;shared, ((unsigned __int128)i &lt;&lt; 64) | i);       \/* inline lock cmpxchg16b *\/\n    return NULL;\n}\n\nstatic void *reader(void *arg) {\n    unsigned long long *torn = arg, loads = 0;\n    while (!atomic_load(&amp;stop)) {\n        unsigned __int128 v = atomic_load(&amp;shared);                    \/* call __atomic_load_16 *\/\n        if ((unsigned long long)(v >> 64) != (unsigned long long)v) (*torn)++;\n        loads++;\n    }\n    torn&#91;1] = loads;\n    return NULL;\n}\n\nint main(void) {\n    unsigned long long res&#91;2] = {0, 0};\n    pthread_t w, r;\n    pthread_create(&amp;w, NULL, writer, NULL);\n    pthread_create(&amp;r, NULL, reader, res);\n    struct timespec t = {2, 0}; nanosleep(&amp;t, NULL);\n    atomic_store(&amp;stop, 1);\n    pthread_join(w, NULL); pthread_join(r, NULL);\n    printf(\"torn loads: %llu of %llu\\n\", res&#91;0], res&#91;1]);\n    return res&#91;0] != 0;\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">If we now build and run that code (run it 3x for 2s each time), we can see how the different compilers fare:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\n+ clang -O2 atomic16.c -lpthread -latomic -o out\/atomic16-clang-latomic\n#    writer: cmpxchg16b   reader: call &lt;__atomic_load_16@plt>\n#    Links to: &#91;libatomic.so.1] &#91;libc.so.6]\n#      __atomic_load_16: U __atomic_load_16@LIBATOMIC_1.0\n+ zig cc -target x86_64-linux-gnu -O2 atomic16.c -lpthread -o out\/atomic16-zigcc\n#    writer: cmpxchg16b   reader: call &lt;__atomic_load_16>\n#    Links to: &#91;libc.so.6] &#91;libpthread.so.0]\n#     __atomic_load_16: t __atomic_load_16\n+ zig cc -target x86_64-linux-gnu -O2 atomic16.c -lpthread -latomic -o out\/atomic16-zigcc-latomic\n#    writer: cmpxchg16b   reader: call &lt;__atomic_load_16>\n#    Links to: &#91;libc.so.6] &#91;libpthread.so.0]\n#    __atomic_load_16: t __atomic_load_16\n\n+ .\/out\/atomic16-clang-latomic\ntorn loads: 0 of 682030142\ntorn loads: 0 of 645687188\ntorn loads: 0 of 692854822\n+ .\/out\/atomic16-zigcc\ntorn loads: 720 of 128380947\ntorn loads: 382 of 138503698\ntorn loads: 13108 of 131805641\n+ .\/out\/atomic16-zigcc-latomic\ntorn loads: 455 of 137924614\ntorn loads: 20909 of 132735414\ntorn loads: 425 of 135945438<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">I used Zig 0.17.0 for these tests.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">The inability to report upstream<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">I wrote about it in more detail <a href=\"https:\/\/www.flamingspork.com\/blog\/2026\/10\/26\/zig-self-hosted-backend-lowers-a-seq_cst-atomicrmw-xchg-with-a-discarded-result-to-a-plain-mov\/\">in my first post on Zig<\/a>, but the tl;dr is that since an LLM was involved in finding this bug, the \u201c<strong>No LLMs for finding bugs\u201d<\/strong> part of their <a href=\"https:\/\/ziglang.org\/code-of-conduct\/\">Code of Conduct<\/a> obviously applies.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">I\u2019ve written this up because others may find it interesting, and maybe this helps someone debug their concurrent Zig application at some point, or learn something about how compilers do (or do not) work.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>The Zig project heavily advocates the use case of using its toolchain to build C\/C++ code, &#8220;Use Zig as a zero-dependency, drop-in C\/C++ compiler that supports cross-compilation out-of-the-box.\u201d (Zig home page https:\/\/ziglang.org\/ fetched 2026-10-21). Back in 2020, the announcement from &hellip; <a href=\"https:\/\/www.flamingspork.com\/blog\/2026\/11\/06\/double-width-atomics-arent-atomic-if-using-zig-cc-to-build-c-c-code\/\">Continue reading <span class=\"meta-nav\">&rarr;<\/span><\/a><\/p>\n","protected":false},"author":2,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_jetpack_newsletter_access":"","_jetpack_dont_email_post_to_subs":false,"_jetpack_newsletter_tier_id":0,"_jetpack_memberships_contains_paywalled_content":false,"_jetpack_hide_featured_image":false,"_jetpack_featured_image_duplicate_dismissed":0,"_jetpack_feature_clip_id":0,"_jetpack_memberships_contains_paid_content":false,"footnotes":"","jetpack_publicize_message":"","jetpack_publicize_feature_enabled":true,"jetpack_social_post_already_shared":true,"jetpack_social_options":{"image_generator_settings":{"template":"highway","default_image_id":0,"font":"","enabled":false},"version":2},"jetpack_post_was_ever_published":false},"categories":[1],"tags":[],"class_list":["post-5048","post","type-post","status-publish","format-standard","hentry","category-general"],"jetpack_publicize_connections":[],"jetpack_sharing_enabled":true,"jetpack_shortlink":"https:\/\/wp.me\/p5a6n8-1jq","jetpack-related-posts":[{"id":5056,"url":"https:\/\/www.flamingspork.com\/blog\/2026\/11\/05\/zig-self-hosted-compiler-cant-compile-a-16-byte-atomicload\/","url_meta":{"origin":5048,"position":0},"title":"Zig self-hosted compiler can&#8217;t compile a 16-byte @atomicLoad","author":"Stewart Smith","date":"2026-11-05","format":false,"excerpt":"Previously, I wrote about how the Zig self-hosted backend lowers a seq_cst @atomicRmw(.Xchg) with a discarded result to a plain mov, but this isn't the only issue with the self-hosted backend and atomics. A valid bit of Zig code is @atomicLoad(u128, p, .seq_cst) - an atomic load of an unsigned\u2026","rel":"","context":"In &quot;General&quot;","block_context":{"text":"General","link":"https:\/\/www.flamingspork.com\/blog\/category\/general\/"},"img":{"alt_text":"","src":"","width":0,"height":0},"classes":[]},{"id":5040,"url":"https:\/\/www.flamingspork.com\/blog\/2026\/10\/26\/zig-self-hosted-backend-lowers-a-seq_cst-atomicrmw-xchg-with-a-discarded-result-to-a-plain-mov\/","url_meta":{"origin":5048,"position":1},"title":"Zig self-hosted backend lowers a seq_cst @atomicRmw(.Xchg) with a discarded result to a plain mov","author":"Stewart Smith","date":"2026-10-26","format":false,"excerpt":"Finding codegen bugs for atomic operations is fun! I'll note that I am not a Zig developer. I don't write code in the language, and I don't work on the language. I'm just some guy who decided to learn a bit about it. The Zig programming language has two compilers\u2026","rel":"","context":"In &quot;General&quot;","block_context":{"text":"General","link":"https:\/\/www.flamingspork.com\/blog\/category\/general\/"},"img":{"alt_text":"","src":"https:\/\/i0.wp.com\/www.flamingspork.com\/blog\/wp-content\/uploads\/2026\/10\/image-2.png?resize=350%2C200&ssl=1","width":350,"height":200,"srcset":"https:\/\/i0.wp.com\/www.flamingspork.com\/blog\/wp-content\/uploads\/2026\/10\/image-2.png?resize=350%2C200&ssl=1 1x, https:\/\/i0.wp.com\/www.flamingspork.com\/blog\/wp-content\/uploads\/2026\/10\/image-2.png?resize=525%2C300&ssl=1 1.5x"},"classes":[]},{"id":1623,"url":"https:\/\/www.flamingspork.com\/blog\/2009\/05\/06\/c-stl-bitset-only-useful-for-known-at-compile-time-number-of-bits\/","url_meta":{"origin":5048,"position":2},"title":"c++ stl bitset only useful for known-at-compile-time number of bits","author":"Stewart Smith","date":"2009-05-06","format":false,"excerpt":"Found in the libstdc++ docs: Extremely weird solutions. If you have access to the compiler and linker at runtime, you can do something insane, like figuring out just how many bits you need, then writing a temporary source code file. That file contains an instantiation of bitset for the required\u2026","rel":"","context":"In &quot;code&quot;","block_context":{"text":"code","link":"https:\/\/www.flamingspork.com\/blog\/category\/code\/"},"img":{"alt_text":"","src":"","width":0,"height":0},"classes":[]},{"id":1908,"url":"https:\/\/www.flamingspork.com\/blog\/2010\/05\/05\/haildb-hudson-compiler-warnings-and-cppcheck\/","url_meta":{"origin":5048,"position":3},"title":"HailDB, Hudson, compiler warnings and cppcheck","author":"Stewart Smith","date":"2010-05-05","format":false,"excerpt":"I've integrated HailDB into our Hudson setup (haildb-trunk on Hudson). I've also made sure that Hudson is tracking the compiler warnings. We've enabled more compiler warnings than InnoDB has traditionally been compiled with - this means we've started off with over 4,300 compiler warnings! Most of those are not going\u2026","rel":"","context":"In &quot;drizzle&quot;","block_context":{"text":"drizzle","link":"https:\/\/www.flamingspork.com\/blog\/category\/work-et-al\/drizzle-work-et-al\/"},"img":{"alt_text":"","src":"","width":0,"height":0},"classes":[]},{"id":3906,"url":"https:\/\/www.flamingspork.com\/blog\/2014\/11\/14\/c-bitfields-considered-harmful\/","url_meta":{"origin":5048,"position":4},"title":"C bitfields considered harmful","author":"Stewart Smith","date":"2014-11-14","format":false,"excerpt":"In C (and C++) you can specify that a variable should take a specific number of bits of storage by doing \"uint32_t foo:4;\" rather than just \"uint32_t foo\". In this example, the former uses 4 bits while the latter uses 32bits. This can be useful to pack many bit fields\u2026","rel":"","context":"In &quot;code&quot;","block_context":{"text":"code","link":"https:\/\/www.flamingspork.com\/blog\/category\/code\/"},"img":{"alt_text":"","src":"","width":0,"height":0},"classes":[]},{"id":3520,"url":"https:\/\/www.flamingspork.com\/blog\/2013\/10\/21\/carbon-footprint-of-interpreted-languages\/","url_meta":{"origin":5048,"position":5},"title":"Carbon footprint of interpreted languages","author":"Stewart Smith","date":"2013-10-21","format":false,"excerpt":"Thought from a good discussion with at Fran\u00c3\u00a7ois at OSDC today, what is the carbon footprint of various languages? He mentioned that the carbon footprint of a new Haskell compiler release is remarkably non-trivial due to every Haskell package in Debian needing to be rebuilt. So, I thought, what's the\u2026","rel":"","context":"In &quot;code&quot;","block_context":{"text":"code","link":"https:\/\/www.flamingspork.com\/blog\/category\/code\/"},"img":{"alt_text":"","src":"","width":0,"height":0},"classes":[]}],"jetpack_likes_enabled":true,"jetpack_featured_media_url":"","_links":{"self":[{"href":"https:\/\/www.flamingspork.com\/blog\/wp-json\/wp\/v2\/posts\/5048","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.flamingspork.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.flamingspork.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.flamingspork.com\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/www.flamingspork.com\/blog\/wp-json\/wp\/v2\/comments?post=5048"}],"version-history":[{"count":2,"href":"https:\/\/www.flamingspork.com\/blog\/wp-json\/wp\/v2\/posts\/5048\/revisions"}],"predecessor-version":[{"id":5059,"href":"https:\/\/www.flamingspork.com\/blog\/wp-json\/wp\/v2\/posts\/5048\/revisions\/5059"}],"wp:attachment":[{"href":"https:\/\/www.flamingspork.com\/blog\/wp-json\/wp\/v2\/media?parent=5048"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.flamingspork.com\/blog\/wp-json\/wp\/v2\/categories?post=5048"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.flamingspork.com\/blog\/wp-json\/wp\/v2\/tags?post=5048"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}