簡體   English   中英

管理 POD 類型的 C++ STL 向量回收的簡單方法

[英]Easy way of managing the recycling of C++ STL vectors of POD types

我的應用程序包含數百萬次調用數十個函數。 在這些函數中的每一個中,一個或幾個 POD(普通舊數據)類型的臨時std::vector容器被初始化、使用,然后被破壞。 通過分析我的代碼,我發現分配和釋放會導致巨大的開銷。

一個懶惰的解決方案是將所有函數重寫為包含這些臨時緩沖區容器作為 class 成員的函子。 然而,這會炸毀 memory 的消耗,因為功能很多且緩沖區大小並非微不足道。

更好的方法是分析代碼,收集所有緩沖區,預先考慮如何最大程度地重用它們,並將最小的共享緩沖區容器集提供給 arguments 等函數。 但這可能是太多的工作。

我想在我未來的所有開發中解決這個問題一次,在此期間臨時 POD 緩沖區變得必要,而不必有太多的預謀。 我的想法是實現一個容器端口,並將對它的引用作為每個可能需要臨時緩沖區的 function 的參數。 在這些函數中,應該能夠從端口獲取任何 POD 類型的容器,並且端口還應該在函數返回之前自動調用容器。

// Port of vectors of POD types.
struct PODvectorPort 
{
  std::size_t Nlent; // Number of dispatched containers.
  std::vector<std::vector<std::size_t> > X; // Container pool.
  PODvectorPort() { Nlent = 0; }
};


// Functor that manages the port.
struct PODvectorPortOffice
{
  std::size_t initialNlent; // Number of already-dispatched containers
  // when the office is set up.
  PODvectorPort *p; // Pointer to the port.
  
  
  PODvectorPortOffice(PODvectorPort &port) 
  { 
    p = &port; 
    initialNlent = p->Nlent;
  }
  
  
  template<typename X, typename Y>
  std::vector<X> & repaint(std::vector<Y> &y) // Repaint the container.
  {
    // return *((std::vector<X>*)(&y)); // UB although works
    std::vector<X> *rst = nullptr;
    std::memcpy(&rst, &y, std::min(
      sizeof(std::vector<X>*), sizeof(std::vector<Y>*)));
    return *rst; // guess it makes no difference. Should still be UB.
  }
  
  
  template<typename T>
  std::vector<T> & lend()
  {
    ++p->Nlent;
    // Ensure sufficient container pool size:
    while (p->X.size() < p->Nlent) p->X.push_back( std::vector<size_t>(0) );
    return repaint<T, std::size_t>( p->X[p->Nlent - 1] );
  }

  
  void recall() { p->Nlent = initialNlent; }
  ~PODvectorPortOffice() { recall(); }
};


struct ArbitraryPODstruct
{
  char a[11]; short b[7]; int c[5]; float d[3]; double e[2];
};


// Example f1():
// f2(), f3(), ..., f50() are similarly defined.
// All functions are called a few million times in certain 
// order in main(). 
// port is defined in main().
void f1(other arguments..., PODvectorPort &port)
{
  
  PODvectorPort portOffice(port);
  
  // Oh, I need a buffer of chars:
  std::vector<char> &tmpchar = portOffice.lend();
  tmpchar.resize(789); // Trivial if container already has sufficient capacity.
  
  // ... do things
  
  // Oh, I need a buffer of shorts:
  std::vector<short> &tmpshort = portOffice.lend();
  tmpshort.resize(456);  // Trivial if container already has sufficient capacity.
  
  // ... do things.
  
  // Oh, I need a buffer of ArbitraryPODstruct:
  std::vector<ArbitraryPODstruct> &tmpArb = portOffice.lend();
  tmpArb.resize(123);  // Trivial if container already has sufficient capacity.
  
  // ... do things.
  
  // Oh, I need a buffer of integers, but also tmpArb is no longer 
  // needed. Why waste it? Cache hot.
  std::vector<int> &tmpint = portOffice.repaint(tmpArb);
  tmpint.resize(300); // Trivial.
  
  // ... do things.
}

盡管代碼在gcc-8.3MSVS 2019中都兼容-O2-Ofast ,並且通過了所有選項的廣泛測試,但由於PODvectorPortOffice::repaint()的駭人聽聞的性質,我預計會受到批評,它會“投射”矢量就地輸入。

上述代碼的正確性和效率的一組充分但非必要條件是:

  1. std::vector<T>存儲 3 個指向底層緩沖區&[0]&[0] +.size()&[0] +.capacity()的指針。
  2. std::vector<T>的分配器調用malloc()
  3. malloc()返回一個 8 字節(或sizeof(std::size_t) )對齊的地址。

那么,如果這對您來說是不可接受的,那么滿足我需求的現代、適當的方法是什么? 有沒有一種方法可以編寫一個管理器,在不違反標准的情況下實現我的代碼的功能?

謝謝!

編輯:我的問題的更多背景。 這些函數主要計算輸入的一些簡單統計數據。 輸入是不同類型和大小的財務參數的數據流。 為了計算統計數據,首先需要更改和重新排列這些數據,因此需要臨時副本的緩沖區。 計算統計數據很便宜,因此分配和解除分配可能會變得相對昂貴。 為什么我想要一個用於任意 POD 類型的管理器? 因為從現在開始 2 周后,我可能開始收到不同類型的數據 stream,它可以是壓縮在一個結構中的一堆原始類型,或者是迄今為止遇到的復合類型的結構。 當然,我希望上層 stream 只發送原始類型的單獨流,但我無法控制這方面。

讓我說我認為這個問題沒有“權威”的答案。 也就是說,您已經提供了足夠的約束,建議的路徑至少是值得的。 讓我們回顧一下要求:

  • 解決方案必須使用std::vector 在我看來,這是最不幸的要求,因為我不會在這里討論。
  • 解決方案必須符合標准,並且不得違反規則,例如嚴格的別名規則。
  • 解決方案必須要么減少執行的分配數量,要么將分配的開銷減少到可以忽略不計的程度。

在我看來,這絕對是自定義分配器的工作。 有幾個現成的選項可以滿足您的需求,例如Boost Pool Allocators 您最感興趣的是boost::pool_allocator 此分配器將為每個不同的 object大小創建一個 singleton “池”(注意:不是 object類型),它會根據需要增長,但在您明確清除之前不會縮小。

這與您的解決方案之間的主要區別在於,對於不同大小的對象,您將擁有不同的 memory 池,這意味着它將使用比您發布的解決方案更多的 memory,但我認為這是一個合理的權衡。 為了最大限度地提高效率,您可以通過創建具有適當大小的每種所需類型的向量來簡單地啟動一批操作。 使用這些分配器的所有后續向量操作都將執行簡單的 O(1) 分配和釋放。 大致在偽代碼中:

// be careful with this, probably want [[nodiscard]], this is code
// is just rough guidance:
void force_pool_sizes(void)
{
    std::vector<int, boost::pool_allocator<int>> size_int_vect;
    std::vector<SomePodSize16, boost::pool_allocator<SomePodSize16>> size_16_vect;
    ...
    
    size_int_vect.resize(100); // probably makes malloc calls
    size_16_vect.resize(200);  // probably makes malloc calls
    ...

    // on return, objects go out of scope, but singleton pools
    // with allocated blocks of memory remain for future use
    // until explicitly purged.
}

void expensive_long_running(void)
{
    force_pool_sizes();

    std::vector<int, boost::pool_allocator<int>> data1;
    ... do stuff, malloc/free will never be called...

    std::vector<SomePodSize16, boost::pool_allocator<SomePodSize16>> data2;
    ... do stuff, malloc/free will never be called...

    // free everything:
    boost::singleton_pool<boost::pool_allocator_tag, sizeof(int)>::release_memory();
}

如果您想進一步提高 memory 的效率,如果您知道某些池大小是互斥的,您可以修改 boost pool_allocator以使用稍微不同的 singleton 后備存儲,它允許您移動 ZCD6917BFZD6198CDE8D06從一個塊大小到另一個。 目前這可能超出了 scope,但升壓代碼本身很簡單,如果 memory 效率至關重要,那么它可能是值得的。

值得指出的是,嚴格的別名規則可能存在一些混淆,尤其是在實現您自己的 memory 分配器時。 有很多關於嚴格別名以及它的作用和不意味着什么的問題。 是一個很好的起點。

關鍵要點是,在低級 C++ 代碼中,它是完全普通且可接受的,以獲取 memory 數組並將其轉換為某些 object 類型。 如果不是這種情況, std::allocator將不存在。 您也不會對std::aligned_storage類的東西有太多用處。 查看cppreferencestd::aligned_storage的示例用例。 創建了一個類似 STL 的static_vector class,它保留了一個對齊的存儲對象數組, aligned_storage對象被重鑄為具體類型。 這沒有什么是“不可接受的”或“非法的”,但在處理時確實需要一些額外的知識和謹慎。

您的解決方案尤其會激怒代碼律師的原因是您正在獲取一種非字符 object 類型的指針並將它們轉換為不同的非字符 object 類型。 這是對嚴格別名規則的一種特別冒犯性的違反,但考慮到您的其他一些選項,這也不是真正必要的。

還要記住,別名 memory 不是錯誤,而是警告。 我不是說 go 對別名很瘋狂,但我說的是,與 C 和 C++ 的所有事情一樣,當你對你的編譯器規則和機器運行有透徹的了解時,有合理的案例來打破你的編譯器規則和機器運行知識,上。 如果事實證明您實際上並不像您想象的那樣了解這兩件事,請為一些非常漫長而痛苦的調試會話做好准備。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM