Kami memiliki pesan hierarkis yang diwakili oleh kelas. Mereka digunakan untuk mengirim pesan antara utas dan komponen dengan membuat serial/deserialisasi. Dalam usecase kami, kami menggunakan std::variant<InnerA, InnherB, ...>, tetapi untuk menyederhanakan, kode kami mirip dengan ini:

class Inner {
  public:
    Inner(uint8_t* array, uint16_t arrayLength) {
        m_payloadLength = arrayLength; // Let's assume arrayLength is always < 256
        memcpy(m_payload.data(), array, arrayLength));
    }
    std::array<uint8_t, 256> m_payload;
    uint16_t m_payloadLength;
}

class Outer {
  public:
    Outer(const Inner& inner): m_inner(inner){};
    Inner m_inner;
}

class OuterOuter {
  public:
    OuterOuter(const Outer& outer): m_outer(outer){};
    Outer m_outer;
}

Jadi untuk membuat objek OuterOuter yang perlu kita lakukan

int main(int argc, char** argv){
   uint8_t buffer[4]  = {1,2,3,4};
   Inner inner(buffer, 4);
   Outer outer(inner);
   OuterOuter outerOuter(outer);
   addToThreadQueue(outerOuter);
}

Sekarang masalahnya, kami menggunakan perangkat tertanam sehingga kami tidak dapat menggunakan memori dinamis dengan malloc dan baru. Sampai sekarang, apakah konten payload akan disalin tiga kali? Sekali untuk pembuatan inner, sekali ketika copy constructor Inner dipanggil di Outer, dan sekali ketika copy constructor Outer dipanggil di OuterOuter? Jika demikian, apakah ada cara untuk menghindari semua penyalinan ini tanpa menggunakan memori dinamis? Jika ada cara untuk menyampaikan maksud saya ke kompiler, mungkin pengoptimalan dapat dilakukan, jika belum mengoptimalkannya.

Idealnya kita akan menghindari kelas OuterOuter mengambil semua argumen konstruksi sub kelas karena pohon kita cukup dalam dan kita menggunakan std::variant. Dalam contoh ini akan menjadi OuterOuter(uint8_t* array, uint16_t arrayLength), Outer(uint8_t* array, uint16_t arrayLength) dan kemudian Outer akan membangun Inner.

4
Xavier Groleau 28 Mei 2021, 17:14

2 jawaban

Jawaban Terbaik

Anda dapat menggunakan inplacer (lihat postingan ini). Kode Anda akan terlihat seperti ini:

#include <type_traits>
#include <array>
#include <cstdint>
#include <cstring>


using namespace std;


template<class F>
struct inplacer
{
    F f_;
    operator std::invoke_result_t<F&>() { return f_(); }
};

template<class F> inplacer(F) -> inplacer<F>;


struct Inner
{
    Inner(uint8_t* data, size_t len)
        : len_(len) // Let's assume arrayLength is always < 256
    {
        memcpy(payload_.data(), data, len*sizeof(*data));
    }

    std::array<uint8_t, 256>    payload_;
    size_t                      len_;
};

struct Outer
{
    template<class T>
    Outer(T&& inner): m_inner(std::forward<T>(inner)) {}

    Inner m_inner;
};

struct OuterOuter
{
    template<class T>
    OuterOuter(T&& outer): m_outer(std::forward<T>(outer)) {}

    Outer m_outer;
};


void addToThreadQueue(OuterOuter const&);

int main()
{
    uint8_t buffer[4]  = {1,2,3,4};
    OuterOuter outerOuter{ inplacer{[&]{ return Inner{buffer, size(buffer)}; }} };
    addToThreadQueue(outerOuter);
    return 0;
}

Pendekatan ini akan membuat Anda tidak terlalu bergantung pada pengoptimalan kompiler. Ini juga akan berfungsi jika ctor Anda memiliki efek samping (atau tidak tersedia untuk dianalisis oleh kompiler dalam unit terjemahan ini).

main:
        sub     rsp, 280
        mov     rdi, rsp
        mov     DWORD PTR [rsp], 67305985
        mov     QWORD PTR [rsp+256], 4
        call    addToThreadQueue(OuterOuter const&)
        xor     eax, eax
        add     rsp, 280
        ret

Edit: inilah solusi serupa (tetapi tanpa inplacer) -- itu tidak akan bekerja dengan agregat, tapi saya yakin dalam kasus Anda itu tidak masalah.

1
C.M. 28 Mei 2021, 17:12

Secara umum, kompiler modern melakukan pekerjaan yang baik dalam mengoptimalkan konstruksi hierarki kelas yang tidak memiliki efek samping pada konstruksinya selain mengisi tata letak memori berkelanjutan.

Misalnya, gcc mengkompilasi sampel Anda menjadi satu kelas:

main:
  sub rsp, 280
  mov eax, 4
  mov rdi, rsp
  mov WORD PTR [rsp+256], ax
  mov DWORD PTR [rsp], 67305985
  call addToThreadQueue(OuterOuter const&)
  xor eax, eax
  add rsp, 280
  ret

lihat di godbolt

Bahkan lebih dari itu, kompiler diperbolehkan untuk melewatkan beberapa efek samping dalam skenario tertentu. Misalnya, dalam contoh berikut, gcc sepenuhnya menghilangkan alokasi heap melalui proses yang disebut "heap elision".

#include <memory>

extern int foo(int);
extern void bar(int);

struct MyStruct {
    int data;

    MyStruct() {
        auto val = std::make_unique<int>(12); 
        data = foo(*val);
    }
};

int main(int argc, char** argv){
   MyStruct x;
   bar(x.data);
}

Menjadi:

main:
  sub rsp, 8
  mov edi, 12
  call foo(int)
  mov edi, eax
  call bar(int)
  xor eax, eax
  add rsp, 8
  ret

lihat di godbolt

Jelas, Anda perlu memeriksa ulang basis kode Anda sendiri, tetapi pengulangan yang biasa tetap: "Tulis kode yang mudah dibaca dan pertahankan terlebih dahulu, dan hanya jika kompiler melakukan pekerjaan yang buruk dengannya, Anda harus repot-repot melompati lingkaran untuk mengoptimalkannya. "

2
Frank 28 Mei 2021, 15:25