[FIXED] Gibt es moderne CPUs, bei denen ein zwischengespeicherter Bytespeicher tatsächlich langsamer ist als ein Wortspeicher?

Ausgabe

Es ist eine verbreitete Behauptung , dass das Speichern von Bytes im Cache zu einem internen Lese-Änderungs-Schreib-Zyklus führen oder auf andere Weise den Durchsatz oder die Latenz beeinträchtigen kann, verglichen mit dem Speichern eines vollen Registers.

Aber ich habe noch nie Beispiele gesehen. Keine x86-CPUs sind so, und ich denke, dass alle Hochleistungs-CPUs auch jedes Byte in einer Cache-Zeile direkt ändern können. Unterscheiden sich einige Mikrocontroller oder Low-End-CPUs, wenn sie überhaupt Cache haben?

( Ich zähle keine wortadressierbaren Maschinen oder Alpha, das byteadressierbar ist, aber keine Byte-Lade- / Speicheranweisungen enthält. Ich spreche von der schmalsten Speicheranweisung, die die ISA nativ unterstützt.)

In meiner Recherche während der Beantwortung Kann moderne x86-Hardware kein einzelnes Byte im Speicher speichern? , fand ich heraus, dass die Gründe, warum Alpha AXP Bytespeicher wegließ, davon ausgingen, dass sie als echte Bytespeicher im Cache implementiert würden, nicht als RMW-Aktualisierung des enthaltenden Wortes. (Also hätte es den ECC-Schutz für den L1d-Cache teurer gemacht, weil er Byte-Granularität statt 32-Bit benötigt hätte).

Ich gehe davon aus, dass Word-RMW während der Übergabe an den L1d-Cache nicht als Implementierungsoption für andere neuere ISAs in Betracht gezogen wurde, die Bytespeicher implementieren.

Alle modernen Architekturen (außer der frühen Alpha) können echte Byte-Lade-/Speichervorgänge in nicht zwischenspeicherbaren MMIO-Regionen (nicht RMW-Zyklen) durchführen, was zum Schreiben von Gerätetreibern für Geräte mit benachbarten Byte-E/A-Registern erforderlich ist. (z. B. mit externen Aktivierungs-/Deaktivierungssignalen, um anzugeben, welche Teile eines breiteren Busses die echten Daten enthalten, wie die 2-Bit-TSIZ (Übertragungsgröße) auf dieser ColdFire-CPU/Mikrocontroller oder wie PCI/PCIe-Einzelbyteübertragungen oder wie DDR SDRAM-Steuersignale, die ausgewählte Bytes maskieren.)

Vielleicht wäre es für ein Mikrocontroller-Design in Betracht zu ziehen, einen RMW-Zyklus im Cache für Byte-Speicher durchzuführen, auch wenn es nicht für ein superskalares Pipeline-Design der Spitzenklasse gedacht ist, das auf SMP-Server / Workstations wie Alpha abzielt?

Ich denke, diese Behauptung könnte von wortadressierbaren Maschinen stammen. Oder von nicht ausgerichteten 32-Bit-Speichern, die mehrere Zugriffe auf viele CPUs erfordern, und von Leuten, die fälschlicherweise davon auf Byte-Speicher verallgemeinern.


Nur um das klarzustellen, erwarte ich, dass eine Bytespeicherschleife an dieselbe Adresse mit denselben Zyklen pro Iteration ausgeführt wird wie eine Wortspeicherschleife. Zum Füllen eines Arrays können 32-Bit-Speicher also bis zu 4x schneller sein als 8-Bit-Speicher. (Vielleicht weniger, wenn 32-Bit-Speicher die Speicherbandbreite sättigen, 8-Bit-Speicher jedoch nicht.) Aber wenn Byte-Speicher keine zusätzliche Strafe haben, erhalten Sie nicht mehr als einen 4-fachen Geschwindigkeitsunterschied. (Oder was auch immer die Wortbreite ist).

Und ich rede von asm. Ein guter Compiler vektorisiert automatisch eine Byte- oder Int-Speicherschleife in C und verwendet breitere Speicher oder was auch immer auf der Ziel-ISA optimal ist, wenn sie zusammenhängend sind.

(Und das Zusammenführen von Speichern im Speicherpuffer könnte auch zu breiteren Commits in den L1d-Cache für zusammenhängende Byte-Speicheranweisungen führen, also ist dies eine weitere Sache, auf die Sie beim Mikrobenchmarking achten sollten.)

; 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)

Oder eine Schleife über ein 8-KB-Array wie dieses, das 1 Byte oder 1 Wort aus jeweils 8 Bytes speichert (für eine C-Implementierung mit sizeof(unsigned int)=4 und CHAR_BIT=8 für 8-KB, sollte aber zu vergleichbaren Funktionen auf jedem kompilieren C-Implementierung, mit nur einer geringfügigen Abweichung, wenn sizeof(unsigned int)keine Potenz von 2 ist). ASM auf Godbolt für ein paar verschiedene ISAs , entweder ohne Unrolling oder mit der gleichen Menge an Unrolling für beide Versionen.

// volatile defeats auto-vectorization
void byte_stores(volatile unsigned char *arr) {
    for (int outer=0 ; outer<1000 ; outer++)
        for (int i=0 ; i< 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<1000 ; outer++)
        for (int i=0 ; i<(1024 / sizeof(unsigned)) ; i++)  // same number of chars
            arr[i*2 + 0] = 123;       // touch every other int
}

Wenn ich die Größen nach Bedarf anpasse, wäre ich wirklich neugierig, ob jemand auf ein System verweisen kann, das word_store()schneller ist als byte_store().
(Wenn Sie tatsächlich Benchmarking durchführen, achten Sie auf Aufwärmeffekte wie dynamische Taktraten und den ersten Durchlauf, der TLB-Fehlschläge und Cache-Fehlschläge auslöst.)

Oder wenn keine tatsächlichen C-Compiler für alte Plattformen existieren oder suboptimalen Code generieren, der den Speicherdurchsatz nicht beeinträchtigt, dann würde jeder handgefertigte Asm einen Effekt zeigen.

Jede andere Art, eine Verlangsamung für Byte-Speicher zu demonstrieren, ist in Ordnung, ich bestehe nicht auf Schrittschleifen über Arrays oder Spam-Schreibvorgänge innerhalb eines Wortes.

Ich wäre auch in Ordnung mit einer detaillierten Dokumentation über CPU-Interna oder CPU-Zyklus-Timing-Nummern für verschiedene Anweisungen. Ich bin jedoch misstrauisch gegenüber Optimierungsratschlägen oder Leitfäden, die auf dieser Behauptung basieren könnten, ohne sie getestet zu haben.

  • Gibt es noch relevante CPUs oder Mikrocontroller, bei denen zwischengespeicherte Bytespeicher eine zusätzliche Strafe haben?
  • Gibt es noch relevante CPUs oder Mikrocontroller, bei denen nicht zwischenspeicherbare Bytespeicher eine zusätzliche Strafe haben?
  • Gibt es noch nicht relevante historische CPUs (mit oder ohne Write-Back- oder Write-Through-Caches), auf die eine der oben genannten Aussagen zutrifft? Was ist das jüngste Beispiel?

zB ist dies bei einem ARM Cortex-A der Fall?? oder Cortex-M? Irgendeine ältere ARM-Mikroarchitektur? Irgendein MIPS-Mikrocontroller oder eine frühe MIPS-Server-/Workstation-CPU? Irgendwelche anderen zufälligen RISC wie PA-RISC oder CISC wie VAX oder 486? (CDC6600 war wortadressierbar.)

Oder konstruieren Sie einen Testfall, der sowohl Ladevorgänge als auch Speicher umfasst, z. B. zeigt er Wort-RMW aus Bytespeichern, die mit dem Ladedurchsatz konkurrieren.

(Ich bin nicht daran interessiert zu zeigen, dass die Speicherweiterleitung von Bytespeichern zu Wortladungen langsamer ist als Wort-> Wort, da es normal ist, dass SF nur dann effizient funktioniert, wenn eine Ladung vollständig im letzten Speicher enthalten ist, um einen davon zu berühren die relevanten Bytes. Aber etwas, das zeigt, dass die Byte->Byte-Weiterleitung weniger effizient ist als Wort->Wort SF, wäre interessant, vielleicht mit Bytes, die nicht an einer Wortgrenze beginnen.)


(I didn’t mention byte loads because that’s generally easy: 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.)

On a load/store architecture like MIPS, working with byte data just means you use lb or lbu to load and zero or sign-extend it, then store it back with sb. (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.

I notice that gcc has sizeof(uint_fast8_t) == 1 on ARM, AArch64, x86, and MIPS. But IDK how much stock we can put in that. The x86-64 System V ABI defines uint_fast32_t 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), uint_fast8_t 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.

Solution

My guess was wrong. Modern x86 microarchitectures really are different in this way from some (most?) other ISAs.

There can be a penalty for cached narrow stores even on high-performance non-x86 CPUs. The reduction in cache footprint can still make int8_t arrays worth using, though. (And on some ISAs like MIPS, not needing to scale an index for an addressing mode helps).

Merging / coalescing in the store buffer between byte stores instructions to the same word before actual commit to L1d can also reduce or remove the penalty. (x86 sometimes can’t do as much of this because its strong memory model requires all stores to commit in program order.)


ARM’s documentation for Cortex-A15 MPCore (from ~2012) says it uses 32-bit ECC granularity in L1d, and does in fact do a word-RMW for narrow stores to update the data.

The L1 data cache supports optional single bit correct and double bit detect error correction logic in both the tag and data arrays. The ECC granularity for the tag array is the tag for a single cache line and the ECC granularity for the data array is a 32-bit word.

Because of the ECC granularity in the data array, a write to the array cannot update a portion of a 4-byte aligned memory location because there is not enough information to calculate the new ECC value. This is the case for any store instruction that does not write one or more aligned 4-byte regions of memory. In this case, the L1 data memory system reads the existing data in the cache, merges in the modified bytes, and calculates the ECC from the merged value. The L1 memory system attempts to merge multiple stores together to meet the aligned 4-byte ECC granularity and to avoid the read-modify-write requirement.

(When they say “the L1 memory system”, I think they mean the store buffer, if you have contiguous byte stores that haven’t yet committed to L1d.)

Note that the RMW is atomic, and only involves the exclusively-owned cache line being modified. This is an implementation detail that doesn’t affect the memory model. So my conclusion on Can modern x86 hardware not store a single byte to memory? is still (probably) correct that x86 can, and so can every other ISA that provides byte store instructions.


Cortex-A15 MPCore is a 3-way out-of-order execution CPU, so it’s not a minimal power / simple ARM design, yet they chose to spend transistors on OoO exec but not efficient byte stores.

Presumably without the need to support efficient unaligned stores (which x86 software is more likely to assume / take advantage of), having slower byte stores was deemed worth it for the higher reliability of ECC for L1d without excessive overhead.

Cortex-A15 is probably not the only, and not the most recent, ARM core to work this way.


Other examples (found by @HadiBrais in comments):

  1. Alpha 21264 (see Table 8-1 of Chapter 8 of this doc) has 8-byte ECC granularity for its L1d cache. Narrower stores (including 32-bit) result in a RMW when they commit to L1d, if they aren’t merged in the store buffer first. The doc explains full details of what L1d can do per clock. And specifically documents that the store buffer does coalesce stores.

  2. PowerPC RS64-II and RS64-III (see the section on errors in this doc). According to this abstract, L1 of the RS/6000 processor has 7 bits of ECC for each 32-bits of data.

Alpha war von Grund auf aggressiv 64-Bit, daher ist eine 8-Byte-Granularität sinnvoll, insbesondere wenn die RMW-Kosten größtenteils vom Speicherpuffer versteckt / absorbiert werden können. (z. B. waren die normalen Engpässe für den meisten Code auf dieser CPU woanders; sein Cache mit mehreren Ports konnte normalerweise 2 Operationen pro Takt verarbeiten.)

POWER / PowerPC64 ist aus dem 32-Bit-PowerPC hervorgegangen und kümmert sich wahrscheinlich darum, 32-Bit-Code mit 32-Bit-Ganzzahlen und -Zeigern auszuführen. (Es ist also wahrscheinlicher, dass nicht zusammenhängende 32-Bit-Speicher für Datenstrukturen durchgeführt werden, die nicht zusammengeführt werden konnten.) Daher ist die 32-Bit-ECC-Granularität dort sehr sinnvoll.


Beantwortet von –
Peter Cordes


Antwort geprüft von –
Pedro (FixError Volunteer)

0 Shares:
Leave a Reply

Your email address will not be published. Required fields are marked *

You May Also Like