скорость ссылок в C ++ - PullRequest
1 голос
/ 25 мая 2009

Я работал над проектом и пытался найти источник большого замедления во времени выполнения и сузил его до одного метода, который мне удалось оптимизировать из логики. Проблема заключается в том, что мое решение заключается в использовании ссылки, которая заставляет другую часть кода работать довольно медленно ... Вопрос, на который я хотел бы ответить, заключается в том, почему внутренний цикл требует гораздо больше времени для оценки, когда карта является ссылкой, а не локальная переменная?

Вот старый способ до оптимизации:

// old method: create an empty map, populate it
// and then assign it back to the path object later
map<int,float> screenline_usage; 

for (int i=0; i<numCandidates; ++i)
{
  // timing starts here. 
  map<int, float>& my_screenline_usage =
    path->get_combined_screenline_usage(legnr, stop_id);
  map<int, float>::iterator it = my_screenline_usage.begin(); 
  for (; it != my_screenline_usage.end(); ++it) 
    screenline_usage[it->first] += usage * it->second; 
  // timing ends here, this block evaluated 4 million times for overall execution time of ~12 seconds
}

// This function call is evaluated 400k times for an overall execution time of ~126 seconds
path->set_zone_screenline_usage(access_mode, zone_id, screenline_usage); 

// TOTAL EXECUTION TIME: 138 seconds. 

Новый способ после оптимизации:

// new method: get a reference to internal path mapping and populate it
map<int, float>& screenline_usage =
  path->get_zone_screenline_usage(access_mode, zone_id);
screenline_usage.clear();

for (int i=0; i<numCandidates; ++i)
{
  // timing starts here
  map<int, float>& my_screenline_usage =
    path->get_combined_screenline_usage(legnr, stop_id);
  map<int, float>::iterator it = my_screenline_usage.begin(); 
  for (; it != my_screenline_usage.end(); ++it) 
    screenline_usage[it->first] += usage * it->second; 
  // timing ends here, this block evaluated 4 million times for overall execution time of ~76 seconds
}

// New method... no need to assign back to path object (0 seconds execution :)
// TOTAL EXECUTION TIME: 76 seconds (62 second time saving) ... but should be able to do it in just 12 seconds if the use of reference didn't add so much time :(

Вот соответствующие подпрограммы, вызываемые из этого кода:

// This is the really slow routine, due to the copy assignment used. 
void set_zone_screenline_usage(int access_mode, int zone_id,
  map<int,float>& screenline_usage)
{
  m_container[access_mode][zone_id] = screenline_usage; 
}

map<int,float>& get_zone_screenline_usage(int access_mode, int zone_id)
{
  return m_container[access_mode][zone_id]; 
}

ПРИМЕЧАНИЯ. Информация о времени предназначена для одного прогона, в котором приведенный выше код оценивается примерно 400 000 раз. Синхронизация выполняется с использованием некоторых классов, которые я создал для доступа к счетчику меток времени RDTSC (да, я знаю, что TSC означает счетчик меток времени), среднее значение numCandidates равно 10, а среднее число элементов, помещенных в карту screenline_usage, равно 25.


ОБНОВЛЕНИЕ: Во-первых, спасибо всем, кто принял участие здесь. Я думаю, что, в конце концов, это вообще не имело никакого отношения к ссылкам на c ++ и было связано с согласованностью кэша. Я заменил приведенный выше оптимизированный код на вектор & и хеш-функцию, реализованную в виде карты переменных-членов

// newest method: get a reference to internal path mapping (as vector) and populate it 
// map<int,int> m_linkNum_to_SlNum declared in header and populated in constructor. 
vector<float>& screenline_usage = path->get_zone_screenline_usage(access_mode, zone_id);

for (int i=0; i<numCandidates; ++i)
{
  // timing starts here
  map<int, float>& my_screenline_usage =
    path->get_combined_screenline_usage(legnr, stop_id);
  map<int, float>::iterator it = my_screenline_usage.begin(); 
  for (; it != my_screenline_usage.end(); ++it) 
    screenline_usage[m_linkNum_to_SlNum[it->first]] += usage * it->second; 
  // timing ends here, this block evaluated 4 million times for overall execution time of ~9 seconds
}

// Newest method... again no need to assign back to path object (0 seconds execution :)
// TOTAL EXECUTION TIME: just 9 seconds (129 second time saving) ... this is even better than using a locally constructed map which took 12 seconds in the inner loop :)

Здесь мне кажется, что, учитывая, что вектор не является локальным, но является непрерывным блоком памяти и что хеш-функция (m_linkNum_to_SlNum) является локальной переменной-членом, этот подход приводит к коду / данным, которые способны помещается в кэш без необходимости выходить в основную память для данных, что приводит к значительному ускорению. Другие выводы, сделанные с учетом этих выводов, высоко ценятся.

Ответы [ 6 ]

4 голосов
/ 25 мая 2009

Возможно, ваш компилятор C ++ способен встроить некоторый код для локальной карты, но не тогда, когда карта является справочной.

3 голосов
/ 25 мая 2009

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

С другой стороны, если ваш вопрос таков: почему быстрее получить ссылку на уже существующую карту и заполнить ее, чем сделать новую карту, то одна из гипотез состоит в том, что она имеет отношение к

 screenline_usage[it->first] += usage * it->second; 

Так как ключ [it-> first] уже существует на карте внутри path-> get_zone_screenline_usage, то это просто операция обновления и не требует выделения памяти. Однако, если screenline_usage - пустая карта, то каждый доступ к новому [it-> first] означает, что он сначала должен выделить новый узел карты из кучи, и это дорого.

2 голосов
/ 25 мая 2009

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

Существует несколько возможностей, которые могут повлиять на производительность. Я сомневаюсь, что это как-то связано с эталонной производительностью C ++.

Первая возможность заключается в том, что, изменяя screenline_usage на ссылку, вы «по сути» получаете указатель на существующий объект. Фактический экземпляр объекта не может быть map<int,float>, это может быть что угодно, что наследуется от карты. Например, это может быть карта с определенной для нее пользовательской функцией компаратора. Или подкласс с логикой синхронизации потоков. Поскольку вы не знаете, какой тип вы получаете от вызова на m_container[access_mode][zone_id], вы вполне можете получить подкласс, который плохо работает при вставке. (Между прочим, вы можете видеть, какой тип вы возвращаете в отладчике.) Принимая во внимание, что, создавая пустой map<int,float>, вы гарантируете, что тип является реальной картой <>, а не подклассом.

Давайте предположим, что вы получаете реальный экземпляр карты <>.

Вторая возможность заключается в том, что в используемом вами конкретном варианте STL функция map::clear() не позволяет эффективно очищать внутренние структуры данных, используемые для поддержки ассоциативных индексов. Насколько я помню, stl :: map <> использует некоторые сложные внутренние структуры хеширования и корзины, чтобы обеспечить эффективные возможности вставки и извлечения. Однако неясно, что происходит с ними, когда вы вызываете clear (). Поэтому возможно, что screenline_usage.clear() приведет к менее эффективной карте для вставки, чем если бы вы начали с пустой карты.

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

map<int, float>& screenline_usage = new map<int,float>();

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

1 голос
/ 25 мая 2009

Ссылки чаще всего закулисны, реализованы в виде указателей. Исключением является то, что если им назначен временный объект, то время жизни значения увеличивается до времени жизни ссылки; по сути, ссылка - это объект. Таким образом, это зависит от:

get_combined_screenline_usage

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

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

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

(как примечание:

map<int, float>::iterator it = my_screenline_usage.begin(); 
for (; it != my_screenline_usage.end(); ++it) 

может быть более эффективным, если записано как

for (map<int, float>::const_iterator it(my_screenline_usage.begin()), end(my_screenline_usage.begin()); it != end; ++it)

)

0 голосов
/ 16 декабря 2010

Что касается моих обновлений, я думаю, что это, скорее всего, проблема согласованности кэша, а не проблема ссылок c ++.

0 голосов
/ 25 мая 2009

Проблема, которую я пытаюсь выяснить, заключается в том, почему внутренний цикл занимает гораздо больше времени, чтобы оценить, когда карта является справочной?

Я думаю, что то, что занимает много времени, - это вызов get_zone_screenline_usage: и причина, по которой это занимает много времени, заключается в том, что определенный элемент еще не существует в m_container и поэтому должен быть создан и вставлен прежде чем он может быть возвращен.

...