Jan 14, 2026

[C++] Cache-friendly

 Just always having int as default if usage is under contract.



Reference:
Object Lifetimes reading minute
alignas(int) unsigned char buffer[sizeof(int)]; // used for provenance contract.

One byte types:

Benchmark shows
char8_t is faster due to optimizer.
this is that a pointer of char, unsigned char, or std::byte type can point to anywhere, thus
data.size() could potentially being modified inside the loop, thus accessing data.size() is necessary.
As for char8_t has no this privilege, thus data.size() is fixed and stored inside a register.

The Problem: "Type Aliasing" & The Strict Aliasing Rule
In C++, the compiler assumes that two pointers of different types do not point to the same memory location. This allows for massive optimizations.

However, there is a historical exception to this rule: char types (char, signed char, unsigned char) and std::byte are allowed to alias anything.
Thus:
For char, unsigned char, and std::byte:
The compiler looks at data[i] = ... and thinks: "Uh oh. Writing to this byte might technically be overwriting the memory where the std::vector object itself keeps its internal size tracker!"
Because a char* can point anywhere, the compiler is forced to be incredibly cautious. It re-reads data.size() from memory on every single iteration of the loop just in case the last write accidentally changed it. This slows things down.

For char8_t:
When char8_t was introduced in C++20 for UTF-8 string data, the committee decided not to give it this "alias anything" superpower. It obeys strict aliasing.
The compiler looks at the loop and thinks: "Writing to a char8_t array cannot possibly change the std::vector's internal size variables because they are different types."




fix: old trick, store the data.size() inside the register ourselves.

or just:


Bitfields is good for saving memory but accessing them are slow (bitmask other values).
also, under meta-programming, type inference of those bitfields type has to be casted.


Reference:

When choosing a container type to use, consider:


L2 cache is used for data and instructions.
i.e. how we code matters the speed, even the results are the same.
e.g.
slower, due to branch and if branch succeed, instructions are replaced in the cache.

faster, due to branch rarely happen, and REPEAT64 has much more instruction than simple count+=d

Summary


Reference:

Always start with DoD; focus on algorithm.

DoD is more packed with data layout, good for caching.

SoA
Reference:
Align with the cache line to avoid false sharing

CS 101, keep false sharing affect in mind.


All-in-all summary


Avoid long branches:

// BAD
void process_data(Data* d) {
    if (!d) {
        // LONG BRANCH / COLD CODE
        // Imagine 100 lines of logging, stack tracing, 
        // and complex error recovery logic here.
        log_error("Null pointer detected...");
        cleanup_subsystems();
        notify_admin_via_snmp();
        throw std::runtime_code("Fatal Error");
    }

    // This "hot" code is now physically far away from the 'if' 
    // check in the compiled binary.
    d->value += 42; 
    d->status = Ready;
}

// Better, avoid I-Cache miss

// Move cold logic to a non-inline function
[[noreturn]] void handle_fatal_error() {
    log_error("Null pointer detected...");
    cleanup_subsystems();
    notify_admin_via_snmp();
    throw std::runtime_code("Fatal Error");
}

void process_data_optimized(Data* d) {
    // C++20 [[likely]]/[[unlikely]] attributes guide the compiler
    if (!d) [[unlikely]] {
        handle_fatal_error(); // A jump to a far-away location happens ONLY on error
    }

    // This code is now physically adjacent to the 'if' check
    d->value += 42; 
    d->status = Ready;
}




No comments:

Post a Comment

Note: Only a member of this blog may post a comment.