title: Are there any modern CPUs where a cached byte store is actually slower than a word store?
body: <p>It's a <a href="https://stackoverflow.com/questions/54200621/can-most-modern-processors-read-or-write-a-single-byte/54202880#54202880">common claim</a> that a byte store into cache may result in an internal read-modify-write cycle, or otherwise hurt throughput or latency vs. storing a full register.</p>
  
  <p><strong>But I've never seen any examples.  No x86 CPUs are like this, and I think all high-performance CPUs can directly modify any byte in a cache-line, too.  Are some microcontrollers or low-end CPUs different, if they have cache at all?</strong></p>
  
  <p>(<strong>I'm not counting word-addressable machines</strong>, or Alpha which is byte addressable but lacks byte load/store instructions.  I'm talking about the narrowest store instruction the ISA natively supports.)</p>
  
  <p>In my research while answering <a href="https://stackoverflow.com/questions/46721075/can-modern-x86-hardware-not-store-a-single-byte-to-memory">Can modern x86 hardware not store a single byte to memory?</a>, I found that the reasons Alpha AXP omitted byte stores presumed they'd be implemented as true byte stores into cache, not an RMW update of the containing word.  (So it would have made ECC protection for L1d cache more expensive, because it would need byte granularity instead of 32-bit).</p>
  
  <p><strong>I'm assuming that word-RMW during commit to L1d cache wasn't considered as an implementation option for other more-recent ISAs that do implement byte stores.</strong></p>
  
  <p>All modern architectures (other than early Alpha) can do true byte loads/stores to uncacheable MMIO regions (not RMW cycles), which is necessary for writing device drivers for devices that have adjacent byte I/O registers.  (e.g. with external enable/disable signals to specify which parts of a wider bus hold the real data, like the 2-bit TSIZ (transfer size) on <a href="https://stackoverflow.com/q/49882244">this ColdFire CPU/microcontroller</a>, or like PCI / PCIe single byte transfers, or like DDR SDRAM control signals that mask selected bytes.)</p>
  
  <p>Maybe doing an RMW cycle in cache for byte stores would be something to consider for a microcontroller design, even though it's not for a high-end superscalar pipelined design aimed at SMP servers / workstations like Alpha?</p>
  
  <p>I think this claim might come from word-addressable machines.  Or from unaligned 32-bit stores requiring multiple accesses on many CPUs, and people incorrectly generalizing from that to byte stores.</p>
  
  <hr>
  
  <p>Just to be clear, I expect that a byte store loop to the same address would run at the same cycles per iterations as a word store loop.  So for filling an array, 32-bit stores can go up to 4x faster than 8-bit stores.  (Maybe less if 32-bit stores saturate memory bandwidth but 8-bit stores don't.)  But unless byte stores have an extra penalty, you won't get <em>more</em> than a 4x speed difference. (Or whatever the word width is).</p>
  
  <p>And I'm talking about asm.  A good compiler will auto-vectorize a byte or int store loop in C and use wider stores or whatever is optimal on the target ISA, if they're contiguous.</p>
  
  <p>(And store coalescing in the store buffer could also result in wider commits to L1d cache for contiguous byte-store instructions, so that's another thing to watch out for when microbenchmarking)</p>
  
  <pre><code>; x86-64 NASM syntax
  mov   rdi, rsp
  ; RDI holds at a 32-bit aligned address
  mov   ecx, 1000000000
  .loop:                      ; do {
      mov   byte [rdi], al
      mov   byte [rdi+2], dl     ; store two bytes in the same dword
        ; no pointer increment, this is the same 32-bit dword every time
      dec   ecx
      jnz   .loop             ; }while(--ecx != 0}
  
  
      mov   eax,60
      xor   edi,edi
      syscall         ; x86-64 Linux sys_exit(0)
  </code></pre>
  
  <p>Or a loop over an 8kiB array like this, storing 1 byte or 1 word out of every 8 bytes (for a C implementation with sizeof(unsigned int)=4 and CHAR_BIT=8 for the 8kiB, but should compile to comparable functions on any C implementation, with only a minor bias if <code>sizeof(unsigned int)</code> isn't a power of 2).  ASM <a href="https://godbolt.org/#z:OYLghAFBqd5QCxAYwPYBMCmBRdBLAF1QCcAaPECAKxAEZSAbAQwDtRkBSAJgCFufSAZ1QBXYskwgA5AHoZAagBuqZgTwNM8rADNMTAoPlMRRALSLMyIsTwAvfXlQsOABgCCyvOnkAjAJ4EmAD6gtaYghDKquqaIiyCeMAsmN7ICEzE8gBUGcQAlPIcAOx87vLl8tok8hB4LATyooHEHADMACIuha08jSaYLa0AwrQuY929TQP8/HmubhWLldW19fJ4bZ0T621D8qNcACzbG7yzS/Jy8gyoqAAOjRaZhwDW2fJcWQm2mKjaqwQCmkMoJ5hcLrkOABWHh4LKfb6/f5xBJJFIFfj7aHtbo42hcVptXoVK5EERpRrJXwBTR/eSYJ5%2BD7yADuJHQoPcxXa83mVyiDg08iIWkwun0RhMqHMlmsdgcThA8gAHgAOABs8mQrHkIkEmjcADUABpcJR3AC2TEELwtqEU6Hmnm8bOI6BCYQiArUQpRiWS3jqBByxHyhRKYIqVUyAL6zU2XSJcemw1G4yTUxaZ14czK4OjNSDOw6iZ6xaGEAOxwUiL%2BED9aPQeQxZdOfBz5SugiYFs0LBEFp8A0a2i16WInIW4MWkJhcLNmJc2Nx%2BwJSYupNEFIZAyZqAICGHQd5RR5XK5BLqyAYIiw3SGoXw9QAdAg2th5kXEaqV7XkUGgm0a0CFVIJASJKQ8kYaQoSkUgWGkFw4NQaQhhmXh5GEMQJEKAlaDgghEMgqCXhAVpWmfciqOomj1WgqRDjgi06DGeCiNIFCpDgwQQBcUhCKkJCoLgWAYEQFBUAtO4YjICgIDQKSZJADILWAVUuD47R1GaHiIB8difDqDI/GkfDSAU3t6gAeRYBgTMEuCsCtNgNHY/BiFlPALB4hzSEwZVLH6Uy4KDTAGHYhg8B8YhjKGDBJCkMyCBsZjEsgxhWHYdCBEinweMgKD7jUJwfNMKzWnkUwLWQO4RE2NBiECZVTCYIoKqq7Ras2ZInG40RxEkWgoPCqRYLY3zOLcAAlABZeRgGQZB5FVZ8zQgXBCGqbhWnoeQ4sUjRMm2rgCjQ7MeAIojm1IUi03oxjSGYqE%2BIQibpG43j%2BKu0gRPEhTpMO8hKH%2BpTr0yopWK0hgdMofTfMMlhjOC8zJMsggbLstzMGc4BXN89zPO89j/MCwJkdCkakMYKKYuIPw4qwZHkrwVL8OGzKUGy6m8vgQq7mK%2BJpDK9qCAyYBMAaC08DuQxTCqKWCDlmLe1MO5UFC4g%2BuwwbhpguDXqpzi1XVUx1WOMG2HkIpnxcG2ag26xcJ20g9skgHh221pTuyy6HOuw8mCwYhKF1himJAZ7xsN96hE%2BgShJIsiKJolOqLokbWn19jON9oSfrE36ICQEHAbkkuBmU4gLTNtSNNIKGYb0gyjLp5GLMwazbPsqmnMyvGe7wDyrC88JiYC5AgrS8h6jCiKadi%2BKmZS4L2bYTnzu5/KID5gXSvKyrqq6joGqalqijGrCBroUOxoN5DpGmmazfmxbltW%2B38Ed7bdv292joJQ43tzq52IjdJOlFU4p3uuHSOd8OIxx4nxeO6VC7FzdjJIG8l0GHRQMwNgENNLaQGLpOGVMEZIynu3TumN8bYz7glAeQ81BE18iTCeZMp4UzntFBejMp7M1ZulPBWUN65S3jvRwgspDCwPhkNImxuwsHQH4aKXhxZayvkNeit9s7SGNqbc2wirY2ztutT%2BW08Iu1/jJJ2UIgHtgul9P2UEA5BxDtAx6EcXq6K4rHJBV1E7kQgZA6i9FM5R3vr45B10RpcCzm9KJATSBPASIqQ4QA%3D%3D%3D" rel="nofollow noreferrer">on Godbolt for a few different ISAs</a>, with either no unrolling, or the same amount of unrolling for both versions.</p>
  
  <pre><code>// volatile defeats auto-vectorization
  void byte_stores(volatile unsigned char *arr) {
      for (int outer=0 ; outer&lt;1000 ; outer++)
          for (int i=0 ; i&lt; 1024 ; i++)      // loop over 4k * 2*sizeof(int) chars
              arr[i*2*sizeof(unsigned) + 1] = 123;    // touch one byte of every 2 words
  }
  
  // volatile to defeat auto-vectorization: x86 could use AVX2 vpmaskmovd
  void word_stores(volatile unsigned int *arr) {
      for (int outer=0 ; outer&lt;1000 ; outer++)
          for (int i=0 ; i&lt;(1024 / sizeof(unsigned)) ; i++)  // same number of chars
              arr[i*2 + 0] = 123;       // touch every other int
  }
  </code></pre>
  
  <p><strong>Adjusting sizes as necessary, I'd be really curious if anyone can point to a system where <code>word_store()</code> is faster than <code>byte_store()</code>.</strong> 
   (If actually benchmarking, beware of warm-up effects like dynamic clock speed, and the first pass triggering TLB misses and cache misses.)</p>
  
  <p>Or if actual C compilers for ancient platforms don't exist or generate sub-optimal code that doesn't bottleneck on store throughput, then any hand-crafted asm that would show an effect.</p>
  
  <p>Any other way of demonstrating a slowdown for byte stores is fine, I don't insist on strided loops over arrays or spamming writes within one word.</p>
  
  <p><strong>I'd also be fine with detailed documentation about CPU internals</strong>, or CPU cycle timing numbers for different instructions.  I'm leery of optimization advice or guides that could be based on this claim without having tested, though.</p>
  
  <ul>
  <li><strong>Any still-relevant CPU or microcontroller where cached byte stores have an extra penalty?</strong></li>
  <li><strong>Any still-relevant CPU or microcontroller where <em>un-cacheable</em> byte stores have an extra penalty?</strong></li>
  <li><strong>Any not-still-relevant historical CPUs (with or without write-back or write-through caches) where either of the above are true?  What's the most recent example?</strong></li>
  </ul>
  
  <p>e.g. is this the case on an ARM Cortex-A?? or Cortex-M?  Any older ARM microarchitecture?  Any MIPS microcontroller or early MIPS server/workstation CPU?  Anything other random RISC like PA-RISC, or CISC like VAX or 486?  (CDC6600 was word-addressable.)</p>
  
  <p><strong>Or construct a test-case involving loads as well as stores, e.g. showing word-RMW from byte stores competing with load throughput.</strong></p>
  
  <p>(I'm not interested in showing that store-forwarding from byte stores to word loads is slower than word->word, because it's normal that SF only works efficiently when when a load is fully contained in the most recent store to touch any of the relevant bytes.  But something that showed byte->byte forwarding being less efficient than word->word SF would be interesting, maybe with bytes that don't start at a word boundary.)</p>
  
  <hr>
  
  <p>(<strong>I didn't mention byte loads because that's generally easy</strong>: access a full word from cache or RAM and then extract the byte you want.  That implementation detail is indistinguishable other than for MMIO, where CPUs definitely don't read the containing word.)</p>
  
  <p>On a load/store architecture like MIPS, working with byte data just means you use <code>lb</code> or <code>lbu</code> to load and zero or sign-extend it, then store it back with <code>sb</code>.  (If you need truncation to 8 bits between steps in registers, then you might need an extra instruction, so local vars should usually be register sized.  Unless you want the compiler to auto-vectorize with SIMD with 8-bit elements, then often uint8_t locals are good...)  But anyway, if you do it right and your compiler is good, it shouldn't cost any extra instructions to have byte arrays.</p>
  
  <p>I notice that gcc has <code>sizeof(uint_fast8_t) == 1</code> on ARM, AArch64, x86, and MIPS.  But IDK how much stock we can put in that.  The x86-64 System V ABI defines <code>uint_fast32_t</code> as a 64-bit type on x86-64.  If they're going to do that (instead of 32-bit which is x86-64's default operand-size), <code>uint_fast8_t</code> should also be a 64-bit type.  Maybe to avoid zero-extension when used as an array index?  If it was passed as a function arg in a register, since it could be zero extended for free if you had to load it from memory anyway.</p>