Почему это так медленнее в С++?

Я преобразовал этот простой метод из С# в С++. Он читает таблицу путей и заполняет список списков ints (или вектор векторов ints).

Пример строки из таблицы путей будет похож на

0 12 5 16 n

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

//Parses the text path vector into the engine
void Level::PopulatePathVectors(string pathTable)
{
    // Read the file line by line.
    ifstream myFile(pathTable);

        for (unsigned int i = 0; i < nodes.size(); i++)
        {
            pathLookupVectors.push_back(vector<vector<int>>());

            for (unsigned int j = 0; j < nodes.size(); j++)
            {
                string line;

                if (getline(myFile, line)) //Enter if a line is read successfully
                {
                    stringstream ss(line);
                    istream_iterator<int> begin(ss), end;
                    pathLookupVectors[i].push_back(vector<int>(begin, end));
                }
            }
        }
    myFile.close();
}

Вот версия С#:

private void PopulatePathLists(string pathList)
{
    // Read the file and display it line by line.
    StreamReader streamReader = new StreamReader(pathList);

    for (int i = 0; i < nodes.Count; i++)
    {
        pathLookupLists.Add(new List<List<int>>());

        for (int j = 0; j < nodes.Count; j++)
        {
            string str = streamReader.ReadLine();
            pathLookupLists[i].Add(new List<int>());

            //For every string (list of ints) - put each one into these lists
            int count = 0;
            string tempString = "";

            while (str[count].ToString() != "n") //While character does not equal null terminator
            {
                if (str[count].ToString() == " ") //Character equals space, set the temp string 
                                                  //as the node index, and move on
                {
                    pathLookupLists[i][j].Add(Convert.ToInt32(tempString));
                    tempString = "";
                }
                else //If characters are adjacent, put them together
                {
                    tempString = tempString + str[count];
                }
                count++;
            }
        }
    }
    streamReader.Close();
}

Извините, это так специфично, но я в тупике.

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

Вот PathTable, который он использует.

EDIT - я попробовал запустить функцию в программе самостоятельно, и это заняло несколько секунд, но я боюсь, что не знаю достаточно, чтобы узнать, как исправить эту проблему. Очевидно, это не код. Что бы это могло быть? Я проверил, где его вызывают, чтобы узнать, есть ли несколько вызовов, но нет. Он находится в конструкторе игрового уровня и вызывается только один раз.

EDIT - я понимаю, что код не самый лучший, как может быть, но здесь дело не в этом. Он работает быстро сам по себе - около 3 секунд, и это хорошо для меня. Проблема, которую я пытаюсь решить, - это то, почему в проекте требуется намного больше времени.

EDIT - я прокомментировал весь код игры, кроме основного игрового цикла. Я поместил этот метод в раздел инициализации кода, который запускается один раз при запуске. Помимо нескольких методов настройки окна, он теперь почти такой же, как и программа с ТОЛЬКО методом, только для STILL требуется около 5 минут для запуска. Теперь я знаю, что это не имеет никакого отношения к зависимостям от pathLookupVectors. Кроме того, я знаю, что это не память, когда компьютер начинает писать на жесткий диск, потому что в то время как медленная программа отлаживает запуск метода, я могу открыть еще один экземпляр Visual Studio и запустить программу с одним методом одновременно в секундах. Я понимаю, что проблема может быть в некоторых основных настройках, но я не испытываю таких извинений, если это разочаровывает в конечном итоге именно поэтому. Я до сих пор не знаю, почему это занимает гораздо больше времени.

Ответ 1

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

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

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

  • Чтобы проверить, потрачено ли дополнительное время на эту подозреваемую функцию, вы можете добавлять инструкции printf в начале и конце функции, которые включают отметки времени. Если это не консольное приложение, а графическое приложение и printfs никуда не отправляются, напишите в файл журнала. Если вы находитесь в Windows, вы можете использовать OutputDebugString и использовать отладчик для захвата printfs. Если вы находитесь в Linux, вы можете записать в системный журнал, используя syslog.

  • Используйте профилировщик исходного кода, чтобы определить, где все это время потрачено. Если разница между вызовом этой функции или нет - это несколько минут, то профилировщик обязательно даст понять, что происходит. Если вы находитесь в Windows, то Very Sleepy - хороший выбор, и если вы работаете в Linux, вы можете использовать OProfile.

Обновить. Таким образом, вы говорите, что сборка выпуска выполняется быстро. Вероятно, это означает, что функции библиотеки, которые вы используете в этой функции, имеют медленные реализации отладки. STL знает, что именно так.

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

  • отключить оптимизацию только для файлов, которые вы хотите отлаживать (убедитесь, что оптимизация остается включенной, по крайней мере, для файла с медленной функцией). Чтобы отключить оптимизацию в файле, выберите файл в обозревателе решений, щелкните правой кнопкой мыши, выберите "Свойства", затем перейдите в "Свойства конфигурации | C/С++/Оптимизация". Посмотрите, как все элементы на этой странице настроены для сборки Debug, и скопируйте все из них в сборке Release. Повторите для всех файлов, которые вы хотите использовать для отладчика.

  • включить отладочную информацию (файл pdb), которая будет сгенерирована. Для этого выберите проект в верхней части обозревателя решений, щелкните правой кнопкой мыши и выберите "Свойства". Затем перейдите в Configuration Properties | Linker | Отладка и скопируйте все настройки из сборки Debug в сборку Release.

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

После завершения отладки вам понадобится reset все эти настройки, конечно.

Надеюсь, это поможет.

Ответ 2

Я профилировал код Very Sleepy (Visual С++ 2010, 32-разрядная Windows XP). Я не знаю, насколько похожи мои исходные данные, но вот результаты в любом случае:

39% времени было потрачено на basic_istream:: operator →

12% basic_iostream:: basic_iostream

9% оператор +

8% _Mutex:: Mutex

5% getline

5% basic_stringbuf:: _ Init

4% locale:: _ Locimp:: _ Addfac

4% вектор:: резерв

4% basic_string:: assign

3% удаление оператора

2% basic_Streambuf:: basic_streambuf

1% Wcsxfrm

5% других функций

Некоторые из вещей, похоже, связаны с встроенными вызовами, поэтому немного сложно сказать, откуда это происходит. Но вы все еще можете получить эту идею. Единственное, что должно делать I/O здесь, это getline, и это занимает всего 5%. Остальное - накладные расходы от операций с потоком и строкой. Потоки С++ медленны, как черт.

Ответ 3

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

Простым и быстрым эквивалентным кодом будет следующее:

int result;
stringstream ss(line);
while ( ss >> result ) //reads all ints untill it encounters non-int
{
    pathLookupVectors[i][j].push_back(result);
}

В С++ такой цикл тоже идиоматичен. Или вместо этого ручного цикла вы можете написать std::copy 1:

std::copy(std::istream_iterator<int>( ss ), 
          std::istream_iterator<int>(), 
          std::back_inserter(pathLookupVectors[i][j]));

1. Это взято из комментария @David.

Или даже лучше, если вы сделаете это, когда вы push_back сам вектор:

 if (getline(myFile, line)) //enter if a line is read successfully
 {
   stringstream ss(line);
   std::istream_iterator<int> begin(ss), end;
   pathLookupVectors[i].push_back(vector<int>(begin, end));
 }

Готово!

Ответ 4

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


Насколько велики ваши строки? Когда вы передаете их в своей версии на С++, вы делаете копии, потому что вы "проходите по значению". Попробуйте передать его по постоянной ссылке:

void Level::PopulatePathVectors(const string &pathTable)

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


Используйте .append или += для расширения tempString. Я считаю, что вы создаете новый строковый объект, а затем заменяете старый на +, а += и .append собираются изменить текущий:

tempString.append(line[count]);

Вы также можете настроить более высокую производительность, объявив свои переменные сверху, а затем переназначив их. Это предотвратит их воссоздание каждый раз. Например, поставьте string line; перед вашим for-loop, потому что он все равно будет перезаписан.

Есть несколько мест, которые вы можете сделать, например tempString.

Ответ 5

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

Плохое профилирование человека.

Пока код работает, просто продолжайте его прерывать. Обычно вы будете видеть один и тот же стек кадров снова и снова.

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

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

Буферизация.

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

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

Обычно я настраиваю простой интерфейс итератора, чтобы убедиться, что все работает. Затем напишите декоратор вокруг него, чтобы читать по 500 строк за раз. Стандартные потоки также имеют встроенные опции буферизации, и они могут быть лучше использованы. Я собираюсь предположить, что их настройки по умолчанию для буферов довольно консервативны.

Резерв.

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

Вы можете на самом деле превзойти производительность std::vector::resize, так как std::vector::resize использует alloc и std::vector::push_back будет использовать realloc

Этот последний бит оспаривается, хотя я читал иначе. У меня нет причин сомневаться в том, что я ошибаюсь, хотя мне придется делать больше исследований, чтобы подтвердить или опровергнуть.

Тем не менее, push_back может работать быстрее, если вы используете резерв с ним.

Разделение строк.

Я никогда не видел решение итератора на С++, которое было эффективным, когда дело касалось файлов gb+. Однако я не пробовал это конкретно. Мое предположение о том, почему они склонны делать много небольших ассигнований.

Вот ссылка на то, что я обычно использую.

Разделить массив символов на два массива символов

Здесь используется рекомендация std::vector::reserve.

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

STL shenanigans.

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

На некоторых платформах при использовании потоков используются по крайней мере некоторые странные потоки безопасности. Поэтому я видел, что незначительное использование std:: cout абсолютно разрушает производительность алгоритма. У вас здесь ничего нет, но если вы ведете журнал в другом потоке, который может создать проблемы. Я вижу a 8% _Mutex::Mutex в другом комментарии, который может говорить о его существовании.

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

В некоторых контейнерах существуют нечетные характеристики производительности. У меня нет проблем с вектором, но я действительно не знаю, что использует istream_iterator внутри. Раньше я отслеживал ошибочный алгоритм, чтобы найти вызов std::list::size, выполняющий полный обход списка с GCC, например. Я не знаю, являются ли более новые версии менее прочными.

Обычную глупую глупость SECURE_CRT следует глупо позаботиться. Интересно, думает ли Microsoft, что мы хотим потратить свое время?

Ответ 6

Оба List.Add и vector::push_back время от времени перераспределяют память по мере роста контейнера. Вектор С++ хранит субвекторы по значению, поэтому все их данные (которые кажутся огромными в вашем случае) копируются снова и снова. Напротив, список С# хранит подписи по ссылке, поэтому данные подписок не копируются во время перераспределения.

Типичная реализация вектора удваивает его емкость при перераспределении. Итак, если у вас есть 1 миллион строк, подвектора будут скопированы log (2,1000000) ≈ 10 раз.

Перенос семантики, введенный в С++ 11, должен устранить этот эффект. До этого попробуйте vector< shared_ptr< vector<int> > >, list< vector<int> > или, если вы заранее знаете размер будущего, используйте vector::reserve(), чтобы избежать перераспределения.

Ответ 7

Не проверял код, но сколько ints он обычно загружает? Рассмотрим, что происходит, когда каждый из ваших vectors достигает своего capacity. A vector растет неэффективно - O (n) Я считаю. С# List не имеет такого поведения.

Рассмотрим использование std::deque, std::list или другого контейнера с улучшенным поведением. Для получения дополнительной информации см. Статью .

Ответ 8

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

Ответ 9

Так как ваша функция не медленна 1 причиной медленной программы должно быть то, что какой-то код, который использует продукт этой функции, становится медленнее при заполнении pathLookupVectors.

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

<суб > 1. Создан в вашем последнем редакторе.