среда, 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++ нет ни того, ни другого – абыдно, да.

6 комментариев:

Miroslav комментирует...

видели листики размалеванные маркерами от Mike Acton ? там есть на что посмотреть ;)
правда другой вопрос "сколько кода надо написать за ночь" может все поменять в сторону мусора, да

eao197 комментирует...

Я видел какую-то такую презентацию, касательно распараллеливания в играх. О ней речь? Или о чем-то другом?

Miroslav комментирует...

там целая серия
на http://cellperformance.beyond3d.com/

ps beware - он консольный фошист (ps3 и тп)

Dmitry Vyukov комментирует...

Не понятно, ты применил сборку мусора к пакету в целом: "каждый обработчик получает ссылку на пакет в виде константного shared_ptr-а". Почему тогда просто не применить это же для каждого поля? Вроде как получается то, что ты хотел...

Dmitry Vyukov комментирует...

Кстати, а действительно необходимо распараллеливание обработки каждого пакета? Просто если пакетов много, то можно давать одному потоку всю обработку пакета от начала до конца. Это устранит все проблемы со сборкой мусора, подсчётом ссылок, и улучшит локальность.

eao197 комментирует...

2Dmitry Vyukov:
Не понятно, ты применил сборку мусора к пакету в целом...Почему тогда просто не применить это же для каждого поля?

a) тогда бы пришлось слишком много всего переписывать;
b) все равно, думаю, множество shared_ptr-ов работало бы медленнее сброщика мусора.

Кстати, а действительно необходимо распараллеливание обработки каждого пакета?

А фиг его знает. Так сложилось в процессе многих итераций добавления все новой и новой функциональности. Может быть, если потратить несколько недель на рефакторинг, то ситуация изменится. Только времени на это не найти :(