Yazılım geliştirme: C++17’de ayırıcılarla optimizasyon

C++17’deki polimorfik ayırıcılar, hem performans hem de belleğin yeniden kullanımı için bellek tahsisinin optimize edilmesine yardımcı olur.

Duyuru

Rainer Grimm uzun yıllardır yazılım mimarı, ekip ve eğitim yöneticisi olarak çalışmaktadır. C++, Python ve Haskell programlama dilleri üzerine makaleler yazmaktan hoşlanıyor, aynı zamanda özel konferanslarda sık sık konuşmaktan da hoşlanıyor. Modern C++ adlı blogunda C++ tutkusunu yoğun bir şekilde ele alıyor.

Aşağıdaki program cppreference.com/monotonic_buffer_resource adresinden gelmektedir. Clang ve MSVC derleyicisi için performans testini genişletip açıklayacağım.

// pmrPerformance.cpp
// https://en.cppreference.com/w/cpp/memory/monotonic_buffer_resource

#include <array>
#include <chrono>
#include <cstddef>
#include <iomanip>
#include <iostream>
#include <list>
#include <memory_resource>
 
template<typename Func>
auto benchmark(Func test_func, int iterations)        // (1)
{
    const auto start = std::chrono::system_clock::now();
    while (iterations-- > 0)
        test_func();
    const auto stop = std::chrono::system_clock::now();
    const auto secs = std::chrono::duration<double>(stop - start);
    return secs.count();
}
 
int main()
{
    constexpr int iterations{100};
    constexpr int total_nodes{2'00'000};
 
    auto default_std_alloc = [total_nodes]            // (2)
    {
        std::list<int> list;
        for (int i{}; i != total_nodes; ++i)
            list.push_back(i);
    };
 
    auto default_pmr_alloc = [total_nodes]            // (3)
    {
        std::pmr::list<int> list;
        for (int i{}; i != total_nodes; ++i)
            list.push_back(i);
    };
 
    auto pmr_alloc_no_buf = [total_nodes]             // (4)
    {
        std::pmr::monotonic_buffer_resource mbr;
        std::pmr::polymorphic_allocator<int> pa{&mbr};
        std::pmr::list<int> list{pa};
        for (int i{}; i != total_nodes; ++i)
            list.push_back(i);
    };
 
    auto pmr_alloc_and_buf = [total_nodes]            // (5)
    {
        // enough to fit in all nodes:
        std::array<std::byte, total_nodes * 32> buffer; 
        std::pmr::monotonic_buffer_resource mbr{buffer.data(), 
          buffer.size()};
        std::pmr::polymorphic_allocator<int> pa{&mbr};
        std::pmr::list<int> list{pa};
        for (int i{}; i != total_nodes; ++i)
          list.push_back(i);
    };
 
    const double t1 = benchmark(default_std_alloc, iterations);
    const double t2 = benchmark(default_pmr_alloc, iterations);
    const double t3 = benchmark(pmr_alloc_no_buf , iterations);
    const double t4 = benchmark(pmr_alloc_and_buf, iterations);
 
    std::cout << std::fixed << std::setprecision(3)
              << "t1 (default std alloc): " << t1 
              << " sec; t1/t1: " << t1/t1 << 'n'
              << "t2 (default pmr alloc): " << t2 
              << " sec; t1/t2: " << t1/t2 << 'n'
              << "t3 (pmr alloc  no buf): " << t3 
              << " sec; t1/t3: " << t1/t3 << 'n'
              << "t4 (pmr alloc and buf): " << t4 
              << " sec; t1/t4: " << t1/t4 << 'n';
}

(1)’deki bu performans testi, (2) – (5)’teki fonksiyonları yüz kez (constexpr int iterations{100}). Her işlev çağrısı bir tane oluşturur std::pmr::list<int> iki yüz bin knot ile (constexpr int total_nodes{2'00'000}). Bireysel listelerin düğümleri farklı şekillerde tahsis edilir:

  • (2): std::list<int> genel kullan operator new
  • (3): std::pmr::list<int> özel hafıza kaynağını kullanır std::pmr::new_delete_resource
  • (4): std::pmr::list<int> kullanılmış std::pmr::monotonic_buffer yığında önceden tahsis edilmiş bir arabellek olmadan
  • (5): std::pmr::list kullanılmış std::pmr::monotonic_buffer yığında önceden tahsis edilmiş bir arabellek ile

Son fonksiyona (5) ilişkin yorum, yığında tüm düğümleri barındıracak yeterli alanın bulunduğunu belirtir: “tüm düğümlere uyacak kadar“. Bu, Linux bilgisayarımda doğruydu, ancak Windows bilgisayarımda değildi. Linux altında yığının standart boyutu 8 MB’tır, ancak Windows altında yalnızca 1 MB’dir. Bu, programımı Windows altında çalıştırmanın MSVC derleyicisini kullandığı anlamına geliyordu. ve clang derleyici sessizce başarısız oldu. Sorunu şunu kullanarak çözdüm: editbin.exe MSVC ve Clang yürütülebilir dosyalarımın yığın boyutunu değiştirdim:

İşte nihayet rakamlar. Referans değeri şu atamadır: std::list<int> (satır 2). Mutlak sayıları değil göreceli sayıları karşılaştırın çünkü sanallaştırılmış bir Linux bilgisayar ve sanal olmayan bir Windows bilgisayar kullandım. Açıkçası maksimum optimizasyonu etkinleştirdim. Bu şu anlama gelir (/Ox) MSVC derleyicisi için ve (-Ox) GCC ve Clang derleyicileri için.

İlginç bir şekilde, hafıza tahsisi hafıza kaynağıyla ilgiliydi std::pmr::new_delete_resource her zaman en yavaş olanıdır. Tam tersine ortaya çıkıyor std::pmr::monotonic_buffer özellikle yığında önceden tahsis edilmiş bir arabellek kullanıldığında en hızlı bellek tahsisini temsil eder. Windows’ta bu, bellek tahsisinin yaklaşık on kat daha hızlı olmasını sağlar.

C++20 ile tanıtılan kavramlar, Ranges kitaplığı, modüller ve eşyordamlar ile birlikte modern C++ uygulamaları oluşturmanın yolunu yeniden tanımladı. 7 – 9 Kasım arası 2023’te Rainer Grimm, yoğun C++20 atölyesinde sizi bilgilendirecek: yeni kavramlar kapsamlı bir şekilde açıklanacak ve C++20’nin sunduğu birçok yararlı işlevi derinlemesine inceleyecek.

Depolama kaynağı std::pmr::new_delete_resource daha da fazla optimizasyon sunar.

std::pmr::monotonic_buffer belleği yeniden kullanmanıza olanak tanır, böylece bellekte yer açmanıza gerek kalmaz.

// reuseMemory.cpp

#include <array>
#include <cstddef>
#include <iostream>
#include <memory_resource>
#include <string>
#include <vector>

int main() {
 
  std::array<std::byte, 2000> buf;

  for (int i = 0; i < 100; ++i) {                          // (1)
    std::pmr::monotonic_buffer_resource pool{buf.data(), 
                        buf.size(),                        // (2)
                        std::pmr::null_memory_resource()};
    std::pmr::vector<std::pmr::string> myVec{&pool};
    for (int j = 0; j < 16; ++j) {                         // (3)
      myVec.emplace_back("A short string");
    }
  }
}

Bu program şunları atar: std::array 2000 bayt ile: std::array<std::byte, 2000>. Bu yığına ayrılmış bellek yüzlerce kez yeniden kullanılır (1). THE std::pmr::vector<std::prm::string> kullan std::pmr::monotonic_buffer_resource yukarı akış depolama kaynağıyla std::pmr::null_memory_resource (2). Son olarak vektöre 16 dize eklenir.

Bu makale, C++17’deki polimorfik bellek kaynakları hakkındaki mini dizimi sonlandırıyor. Bir sonraki yazımda üç yıl ileriye atlayıp C++20 yolculuğuma devam edeceğim.


(kendim)

Haberin Sonu

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir