среда, 28 октября 2009 г.

[comp.prog.thoughts] В очередной раз захотелось иммутабельности и сборщика мусора

(практически в продолжение предыдущей заметки)

С некоторых пор использование new в многопоточных C++ программах вызывает у меня некоторый озноб. Как только пишешь вызов new или делаешь копирование объектов, за которыми прячутся new, так сразу в мозгу щелкает: “здесь будет синхронизация между потоками”. В большинстве случаев на это забивашь, руководствуясь тем, что сначала нужно сделать корректную программу, а уже потом превратить ее в быструю. Но иногда приходится сожалеть о том, что в C++ чего-то не хватает. Вот как в этом случае.

Итак, есть некоторый софт, который берет на входе некоторый пакет данных, проводит его через цепочку обработчиков, каждый из которых генерирует какой-то новый пакет и т.д. – эдакий data-flow получается. На некоторых стадиях пакет может пойти по сразу нескольким обработчикам параллельно. Это важно.

Сам пакет можно условно представить в виде C++ структуры с большим количеством полей типа std::string:

struct data_package_t
  {
    std::string m_field_01;
    std::string m_field_02;
    ...
    std::string m_field_NN;
  };

Из-за того, что пакет может одновременно обрабатываться несколькими обработчиками, каждый обработчик получает ссылку на пакет в виде константного shared_ptr-а:

void some_handler_t::handle_package(
  const shared_ptr< data_package_t > package )
  {
    ... // Соотвественно, внутри обработчика есть только
        // константная ссылка на data_package_t.
  }

Так вот, иногда в handle_package нужно заменить значения одного или двух полей. Из-за того, что исходный пакет доступен как константа, то для замены значения одного поля приходится делать копию всего пакета. Например, вот как часто бывает:

void some_handler_t::handle_package(
  const shared_ptr< data_package_t > package )
  {
    mutable_copy_on_demand_t< data_package_t > obj( *package );
    if( need_for_new_fields_value(...) )
      {
        obj.changeable().m_field_01 = new_value_01();
        obj.changeable().m_field_02 = new_value_02();
      }

    process_package( obj.immutable() );
  }

Т.е., чтобы заменить одно-два поля, приходится копировать весь пакет. А значит, будут скопированы все поля в нем. Что не есть хорошо.

Вероятно, для C++ найдется несколько способов, которые будут способны снизить накладные расходы в этом случае. Но их нужно придумывать. Тогда как при наличии сборки мусора эта ситуация разрешилась бы автоматически. Допустим, есть некоторый язык типа D2, у которого все сложные объекты ссылочные. И есть какая-то хорошая форма иммутабельности (константности). Тогда можно сделать так:

struct data_package_t
  {
    // Это все ссылки.
    immutable string m_field_01;
    immutable string m_field_02;
    ...
    immutable string m_field_NN;

    // Поверхностное копирование.
    data_package_t
    shallow_copy() immutable
      {
        data_package_t result = new data_package_t();
        result.m_field_01 = m_field_01;
        result.m_field_02 = m_field_02;
        ...
        result.m_field_NN = m_field_NN;

        return result;
      }
  };
...
void some_handler_t::handle_package(
  immutable data_package_t package )
  {
    shallow_copy_on_demand_t< data_package_t > obj( package );
    if( need_for_new_fields_value(...) )
      {
        obj.changeable().m_field_01 = new_value_01();
        obj.changeable().m_field_02 = new_value_02();
      }

    process_package( obj.readonly() );
  }

В таком случае вместо копирования всех полей были бы просто продублированы ссылки для N полей + накладные расходы на создание нового экземпляра пакета.

Сборка мусора сейчас есть практически во всех мейнстримовых (или около того) языках. А вот иммутабельности данных – практически нет. В D2 есть, в OCaml (если не ошибаюсь) тоже. В C++ нет ни того, ни другого – абыдно, да.

Отправить комментарий