Showing posts with label cpp_memory. Show all posts
Showing posts with label cpp_memory. Show all posts

Mar 11, 2022

[C++] memory allignment wrap up

Reference:
Björn Andrist - C++ High Performance(2nd)



Memory Alignment

  1. CPU reads memory into its registers one word at a time.
  2. The word size is 64 bits on a 64-bit architecture, 32 bits on a 32-bit architecture, and so forth.
  3. For the CPU to work efficiently when working with different data types, it has restrictions on the addresses where objects of different types are located.
  4. Every type in C++ has an alignment requirement that defines the addresses at which an object of a certain type should be located in memory.
  5. If the alignment of a type is 1, it means that the objects of that type can be located at any byte address. If the alignment of a type is 2, it means that the number of bytes between successive allowed addresses is 2.
    Quote:
    "An alignment is an implementation-defined integer value representing the number
    of bytes between successive addresses at which a given object can be allocated."
  6. use alignof to find out the alignment of a type:
      // Possible output is 4
    std::cout << alignof(int) << '\n';
  7. Use std::align() and not modulo to check the alignment of an object.
    <bit>
    std::has_single_bit Checks if x is an integral power of two. 
      
      bool is_aligned(void* ptr, std::size_t alignment) {
        assert(ptr != nullptr);
        assert(std::has_single_bit(alignment)); // Power of 2
    
        auto s = std::numeric_limits<std::size_t>::max();
        auto aligned_ptr = ptr;
        std::align(alignment, 1, aligned_ptr, s);
    
        return ptr == aligned_ptr;
    }
    Another code tip from CacheLib/HotHashDetector.h
    // Enforce that the number of buckets is a power of two.
      assert((numBuckets & (numBuckets - 1)) == 0);
      
  8. new and malloc() are guaranteed to always return memory suitably aligned for any scalar type.
  9. The <cstddef> header provides us with a type called std::max_align_t, whose alignment requirement is at least as strict as all the scalar types.
    auto* p = new char{};
    auto max_alignment = alignof(std::max_align_t);
    assert(is_aligned(p, max_alignment)); // True
      
    Let's allocate char two times in a row with new:
    auto* p1 = new char{'a'};
    auto* p2 = new char{'b'};
    Then, the memory may look something like this:
    The space between p1 and p2 depends on the alignment requirements of std::max_align_t
  10. It is possible to specify custom alignment requirements that are stricter than the default alignment when declaring a variable using the alignas specifier.

    Let's say we have a cache line size of 64 bytes and that we, for some reason, want to ensure that two variables are placed on separate cache lines. We could do the following:
    alignas(64) int x{};
    alignas(64) int y{};
    // x and y will be placed on different cache lines
  11. It's also possible to specify a custom alignment when defining a type.
    The following is a struct that will occupy exactly one cache line when being used:
    struct alignas(64) CacheLine {
    std::byte data[64];
    };

  12. The stricter alignment requirements are also satisfied when allocating objects on the heap. In order to support dynamic allocation of types with non-default alignment requirements, C++17 introduced new overloads of operator new() and operator
  13. delete() which accept an alignment argument of TAG type std::align_val_t.
  14. There is also an C11 aligned_alloc() function defined in <cstdlib> which can be used to manually allocate aligned heap memory.
    e.g.
    constexpr auto ps = std::size_t{4096};
     // Page size
    struct alignas(ps) Page {
        std::byte data_[ps];
    };
    
    auto* page = new Page{};
    assert(is_aligned(page, ps));
    
    // Use page ...
    delete page;
  15. Memory pages are not part of the C++ abstract machine, so there is no portable way to programmatically get hold of the page size of the currently running system.
  16. However, you could use boost::mapped_region::get_page_size() or a platform- specific system call, such as getpagesize(), on Unix systems. 
  17. A final caveat to be aware of is that the supported set of alignments are defined by the implementation of the standard library you are using, and not the C++ standard.

Type size padding

Reference:

  1. The compiler sometimes needs to add extra bytes, padding, to our user-defined types.
    e.g
    class Document {
        bool is_cached_{};
        double rank_{};
        int id_{};
    };
    
    // turns to
    class Document {
        bool is_cached_{};
        std::byte padding1[7]; // Invisible padding inserted by compiler
        double rank_{};
        int id_{};
        std::byte padding2[4]; // Invisible padding inserted by compiler
    };

    Better:
    class Document {
        double rank_{}; // Rearranged data members
        int id_{};
        bool is_cached_{};
    };
    
    // thus
    class Document {
        double rank_{};
        int id_{};
        bool is_cached_{};
        std::byte padding[3]; // Invisible padding inserted by compiler
    };
  2. As a general rule, you can place the biggest data members at the beginning and the smallest members at the end.
  3. From a performance perspective, there can also be cases where you want to align objects to cache lines to minimize the number of cache lines an object spans over.
  4. While we are on the subject of cache friendliness, it should also be mentioned that it can be beneficial to place multiple data members that are frequently used together next to each other. i.e std::mutex as first data member to avoid whole type ping-pong effect.
  5. A standard data type that needs 16-byte alignment (long long for example), malloc already guarantees that your returned blocks will be aligned correctly.
    Section 7.20.3 of C99 states The pointer returned if the allocation succeeds is suitably aligned so that it may be assigned to a pointer to any type of object.
    https://vsdmars.blogspot.com/2021/03/goticket-make-64-bit-fields-64-bit.html
    void *malloc16 (size_t s) {
        unsigned char *p;
        unsigned char *porig = malloc (s + 0x10);   // allocate extra
        if (porig == NULL) return NULL;             // catch out of memory
        // adds 16 to the address then sets the lower 4 bits to 0, 
        // in effect bringing it back to the next lowest alignment point
        // (the +16 guarantees it is past the actual start of the maloc'ed block).
        p = (porig + 16) & (~0xf);                  // insert padding
        *(p-1) = p - porig;                         // store padding size
        return p;
    }
    
    void free16(void *p) {
        unsigned char *porig = p;                   // work out original
        porig = porig - *(porig-1);                 // by subtracting padding
        free (porig);                               // then free that
    }

Feb 12, 2022

[C++][notes] Branchless Programming in C++ - Fedor Pikus - CppCon 2021

Reference:
Branchless Programming in C++ - Fedor Pikus - CppCon 2021
Computer Architecture - A quantitative approach (Hennessy, Patterson) Appendix-C 
https://vsdmars.blogspot.com/2016/01/likely-or-unlikely-easy-misleading.html


What determines performance?

  • Optimal algorithm
    Get the result with minimal work.
  • Efficient use of language
    do not do any unnecessary work
  • Efficient use of hardware
    use all available resources
    at the same time
    all the time


Hazards

  • Structural; use stall/bubble
  • Data; forwarding, stall/bubble in the middle of pipeline
  • Branch;
    Freeze/flush; holding or deleting any instructions after the branch until the branch destination is known.
    Treat every branch as not taken.
    Treat every branch as taken.
    Delayed branch.
As for branch hazards, we usually use static branch prediction by profiling.
A branch is usually bimodally distributed.
As for dynamic branch prediction; we use branch history table.


Tools

Google benchmark is our friend https://github.com/google/benchmark
$ perf stat our_binary  // shows branch-misses

When do benchmark like this(branch mis-predicting), we should avoid the predicting is done by the compiler, which is damn smart to generate efficient code.
  • Optimizing away branches almost always results in doing more work.
  • CPU usually has idle compute resources which it can handle a bit of extra work.
  • Branch mis-prediction is very expensive.
  • Trade off between the extra work vs. the code of the branch is usually impossible to predict; must be measured.


Less branch means better

Tricks:
use hashmap[] as branches. hashmap is O(1).
However; keep in mind this cause extra memory thus use iff branch is poorly predicted and the extra hashmap computations are small.
  • Sometimes the compiler will do a branchless transformation for you. (conditional move instruction)
  • Compiler's branchless optimization is usually better than ours'.
  • This is almost always branchless in reality:
    return cond ? x : y;
  • Never optimize code preemptively.
  • Optimize only if the profiler shows high mis-prediction rate.
  • Optimizations depend on the compiler.
 if (b) s += x;
 //vs.
 s += b * x;
  • Sometimes branchless code is not really branchless
  • Indirect function calls are similar to branches
    if (cond) f1(); else f2();
  • Can be converted to branchess:
    funcptr f[2] = {&f2, &f1};
    (f[cond])();
  • Above code almost never works.
    If f1 and f2 were inlined..
  • Always measure


Lessons learned

  • Predicted branches are cheap
  • Mis-predicted branches are very expensive - pipeline flush
  • Optimization - user fewer(or zero) branches
  • Always use profiler to detect and validate optimization locations
  • Don't fight with the compiler - sometimes it does the job for you

Jun 4, 2018

[C++][cppcon 2018][note] “C++17's std - -pmr Comes With a Cost" - David Sankel



What std::pmr trying to solve?
  • Allocation is slow
  • Fragmentation hurts performance
  • pre-C++17 allocator is overly complicated

Speed
The memory_resource should have global lifetime.
When passing pointer into function(including constructor), always consider the lifetime of the pointer it points to.


Ok, here's the tricky part.
Consider code below presented by David Sankel:
int main() {
    static Loggingresource memoryResource(
        std::pmr::new_delete_resource()}; // why static? why not local?
    
    std::pmr::set_default_resource(&memoryResource);
    
    std::pmr::vector<int> ints;
    ints.push_back(42);
}

The reason for why making memoryResource global variable? It's due to in different TU(translation unit), functions could have static variables as well, and the init. sequence of this static variable depends on when the function being called. If the static variable of a function from other TU using std::pmr::vector, the destruction sequence of it is later then local variable of main function.

This reminds us the well known article from Alexander Bernauer: RAII vs. exit()

ALWAYS CONSIDER VARIABLE CONSTRUCT/DESTRUCT SEQUENCE

Refer to Linking notes and How main() is executed on Linux
  1. starts up program threading
  2. call _init()
    __attribute__((constructor))
  3. **registers** the _fini() and _rtld_fini(). called after program terminiates.
    __attribute__((destructor))
  4. call main()
class Bar2 {
    std::string data{"data"};
};


class Foo2 {
    std::unique_ptr<Bar2, 
         polymorphic_allocator_delete /* our deleter taking memory_resource ptr */> 
             d_bar;
    
    public:
        Foo2() : d_bar(nullptr /* init. with null */, {{std::pmr::get_default_resource()}}) {
            std::pmr:polymorphic_allocator<bar2> alloc{
                std::pmr::get_default_resource()};
            Bar2 *const bar = alloc.allocate(1); // 1 as one Bar2 instance. Not sizeof(Bar2)
            // Be ware here, we should have a try/catch here
            // since Bar2 constructor can throw.
            // and manually call alloc.deallocate 
            // Remember Effective C++ Item 52, placement new
            // and placement delete should go in pairs.
            alloc.construct(bar);
            d_bar.reset(bar);        
        }
};
To the question from audience asking why using polymorphic allocator instead of using placement new?

Because, because...
operator new has scope...
there's deleting destructor...
Allocator gives us a uniform way to grab memory from heap.

Destructor should NOT throw.
It's hard, but take that into consideration, seriously.

Recap:
[C++11] destructor with noexcept
[C++] Exception in detail.


std::pmr::polymorphic_allocator::destroy noexcept(false)
std::pmr::polymorphic_allocator::deallocate noexcept(false)
class polymorphic_allocator_delete{
    public:
        polymorphic_allocator_delete(
            std::pmr::polymorphic_allocator<std::byte> allocator)
            : d_allocator(std::move(allocator) /* actually, a copy, not move */) 
            {}
        template<typename T>
        void operator() (T *ptr) {
            std::pmr::polymorphic_allocator<T>(d_allocator).destroy(ptr);
            std::pmr::polymorphic_allocator<T>(d_allocator).deallocate(ptr, 1);
        }
        
    private:
        std::pmr::polymorphic_allocator<std::byte> d_allocator;
};
std::pmr::polymorphic_allocator::polymorphic_allocator has no move constructor

With this implementation, the size being allocated is larger with extra 4 words due to unique_ptr has captured a pointer to deleter.

Try using type instead of using ptr to function, which prior has only 1 byte.
Ref: [C++14] unique_ptr with type erasure as shared_ptr

Most of the STL container has a std::pmr namespace version.

Do NOT change the default global memory_resource during the run time. It's dangerous.

We can use std::byte or void for polymorphic_allocator:
std::pmr::polymorphic_allocator<std::byte>
std::pmr::polymorphic_allocator<void>
By using void is align with 'new' operator, which returns void*


Strong exception safety

IFF value type's move constructor is noexcept, otherwise container will go back using value type's copy constructor instead. When? For std::vector, while it need's more slots.

class Foo {
    std::pmr::polymorphic_allocator<std::byte> d_allocator;
    std::unique_ptr<Bar2, 
        polymorphic_allocator_delete
           /* our deleter taking memory_resource ptr */> 
               d_bar;
    
    public:
        // Let's focus on move constructor

        // passing in new allocator
        Foo(Foo&& rhs, std::pmr::polymorphic_allocator<std::byte> allocator);
        Foo(Foo&& rhs) noexcept : 
        d_allocator(other.d_allocator /* no need move because it's copy ptr anyway */), 
        d_bar(nullptr, {d_allocator /* Make sure d_allocator is declared before d_bar*/})
        {
            d_bar.reset(other.d_bar.release());
        }
};
So, can we get rid of d_allocator? Since it's just a placeholder for global memory_resource.

Yes, we can.

How? The data member has the memory_resource which shares it. We can grab it from there.

Sum up

Excerpt from the talk, allocator awareness best practices:
  • Fix allocator at construction
  • Allocator argument to constructor, copy constructor, move constructor, and move copy constructor passing allocator to data members(containers)
  • move constructor takes extra new allocator argument
  • A member type of std::pmr::polymorphic_allocator<std::byte>
  • get_allocator call from data member to get the shared memory_resource
  • Always use global storage for the default allocator(arguable..)
  • Set the default allocator only in main(reasoning due to static namespace variable in different TU init. sequence isn't guaranteed.
It's becoming complicated...
The doctrine to keep in mind is that, fix allocator in the code path. If it's not, there is a problem.

I do not think this pmr adding is mature enough for all data type instances since it's not being supported by all std:: datatypes and corner cases needs to be concerned. Yet, it could; however, being used in scope to tackle with certain problems.
i.e memory pool

Reference:
How tcmalloc Works
jemalloc

Sep 3, 2017

[c++][memo] cast char* to T* violates memory alias.


Casting a pointer to some type T to a pointer to a char type does not violate strict aliasing.
Casting a pointer to a char type to a pointer to an unrelated type does break strict aliasing.

Mar 22, 2012

[c++]PAGESIZE

Detecting the virtual memory page size

Boost:
Class mapped_region
#include <boost/interprocess/mapped_region.hpp>
static std::size_t get_page_size() ;

posix:
long pagesize = sysconf(_SC_PAGE_SIZE);


Windows:
#include <sys/param.h> // defines PAGESIZE