Возвращая карту с помощью getter в проблемах производительности C++

У меня есть класс, который имеет 3-4 члена данных типа std::map<string, vector<string>> который используется для кэширования данных. Его экземпляр создается один раз, а данные заполняются на всех картах из сервисного вызова. У меня есть функции получения, которые используются для доступа к этим картам. (в нем также есть некоторая логика блокировки потоков)

Эти функции получения вызывают много раз, и я беспокоюсь о производительности, так как объекты карты копируются много раз.

class A {
  private:
    map<string, vector<string>> m1;
    map<string, vector<string>> m2;
    map<string, vector<string>> m3;

  public:
    map<string, vector<string>> getm1() { return m1; }
    map<string, vector<string>> getm2() { return m2; } 
    map<string, vector<string>> getm3() { return m3; }
}

class B {
   B() { }
   static A a;

   map<string, vector<string>> getm1() { return a.m1; }
   map<string, vector<string>> getm2() { return a.m2; } 
   map<string, vector<string>> getm3() { return a.m3; } 
} 

Есть несколько раз, когда эти функции получателя вызываются из класса B, Имея промежуточные знания по cpp, я знаю, что получатель вернет всю карту по значению. Но было бы лучше передать его по ссылке или с помощью общего указателя на карту, например, хранения переменных-членов m1, m2, m3 как shared_ptr<map>

  shared_ptr<map<string, vector<string>>> getm1() { return a.m1; } 

Разве это не проблема производительности, и компилятор позаботится об этом?

Прочитав немного об оптимизации возвращаемого значения и немного поняв его, компиляторы могут справиться с некоторыми оптимизациями. Это часть RVO?

Заранее спасибо.

2 ответа

Вернуть ссылки (&) чтобы const карты.

class A {

    using Map_type = map<string, vector<string>>;
    Map_type m1;
    Map_type m2;
    Map_type m3;

  public:
    Map_type const & getm1() const { return m1; }
    Map_type const & getm2() const { return m2; } 
    Map_type const & getm3() const { return m3; }
};

Это позволит вызывающей функции "только для чтения" получить доступ к картам без уплаты стоимости копии.

Если у вас есть внутренняя блокировка, вы не можете просто возвращать ссылки на ваши кэшированные карты, не вызывая условий гонки. Вы также должны предоставить вызывающей стороне средства для блокировки данных от модификации. Если вы ищете эффективность, то один дизайн, который нужно рассмотреть, выглядит примерно так:

class Cache
{
public:
    using mutex_type   = std::shared_timed_mutex;
    using reading_lock = std::shared_lock<mutex_type>;
    using writing_lock = std::unique_lock<mutex_type>;

    using map_type = std::map<std::string, std::vector<std::string>>;

    reading_lock lock_for_reading() const { return reading_lock{mtx}; }
    writing_lock lock_for_writing()       { return writing_lock{mtx}; }

    map_type const& use() const { return m; }

private:

    void update_map()
    {
        // protect every update with a writing_lock
        auto lock = lock_for_writing();

        // safely update the cached map
        m["wibble"] = {"fee", "fie", "foe", "fum"};
    }

    mutable mutex_type mtx;
    map_type   m = {{"a", {"big", "wig"}}, {"b", {"fluffy", "bunny"}}};

};

int main()
{
    Cache cache;

    { // start a scope just for using the map

        // protect every access with a reading_lock
        auto lock = cache.lock_for_reading();

        // safely use the cached map
        for(auto const& s: cache.use().at("a"))
            std::cout << s << '\n';

    } // the lock is released here

    // ... etc ...
}

Делая блокировку доступной для вызывающего абонента, вы позволяете ему защитить данные от состояния гонки во время их чтения. Используя обе блокировки чтения и записи, вы повышаете производительность, поскольку знаете, что вызывающие абоненты не будут изменять кэшированные данные.

Внутренне, когда вы обновляете кэшированные карты, вам нужно использовать writing_lock, чтобы убедиться, что никто не читает их во время их обновления.

Ради эффективности вы, вероятно, хотите отдельное mutex для каждой карты, но это зависит от конкретной динамики вашей ситуации.

Примечание. Это решение возлагает на вызывающего абонента ответственность за правильную блокировку данных. Здесь предлагается более надежное (и сложное) решение: идея GSL по обеспечению безопасности многопоточного кода с помощью примера реализации здесь: gsl_lockable.

Другие вопросы по тегам