Почему VS2008 std :: string.erase () перемещает свой буфер?

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

Итак, что у меня есть:

 std::string line;
 line.reserve(1024);
 std::ifstream file(filePath);
 while(file)
 {
    std::getline(file, line);
    if(line.substr(0, 8) == "Whatever")
    {
        // Do something ...
    }
 }

Хотя это не критичный для производительности код, я вызвал line.reserve (1024) перед операцией синтаксического анализа, чтобы предотвратить многократное перераспределение строки при считывании более крупных строк.

Внутри std :: getline строка стирается перед добавлением к ней символов из каждой строки. Я прошел через этот код, чтобы убедиться, что память не перераспределяется на каждой итерации, и то, что я обнаружил, поджарило мой мозг.

Глубоко внутри string :: erase вместо того, чтобы просто сбрасывать переменную размера в ноль, на самом деле она вызывает memmove_s со значениями указателя, которые перезаписывают используемую часть буфера неиспользуемой частью буфера, следующей сразу за ней, за исключением того, что memmove_s выполняется вызывается с аргументом счетчика, равным нулю, т. е. с запросом перемещения нулевых байтов.

Вопросы:

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

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

И почему он вообще это делает?

Дополнительный вопрос: что за тег стандартной библиотеки C ++?


person Neutrino    schedule 17.11.2011    source источник
comment
STL (несколько ошибочно, но приемлемо), std или stdlib.   -  person Joe McGrath    schedule 17.11.2011
comment
Ах, я попробовал C ++ - std-library, и она не разрешилась. Так что привык к пространству имен, я забыл, что это просто аббревиатура :)   -  person Neutrino    schedule 18.11.2011


Ответы (3)


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

Ошибка подключения:" std::string::erase тупо медленно стирает до конца, что влияет на std::string::resize "

В стандарте ничего не говорится о сложности каких-либо std::string функций, кроме swap.

person Ben Voigt    schedule 17.11.2011
comment
+1 за обнаружение ошибки компилятора. Не многие люди могут это сделать. - person FailedDev; 17.11.2011
comment
Ну, не совсем компилятор, а ошибка реализации STL. Я просто хотел указать на инициалы отвечающего парня с РС - какое совпадение: D - person Voo; 17.11.2011
comment
+1 Хороший отчет, Бен. Судя по всему, я видел это, когда вы изначально сообщали, поскольку я уже проголосовал за него в Connect. :-П - person ildjarn; 17.11.2011
comment
Спасибо. С выводами, полученными в результате этих ответов и более подробным рассмотрением, становится ясно, что в случае с моим кодом перемещение никогда не произойдет, поэтому, как вы говорите, реализация библиотеки неоптимальна, но в данном случае мне не о чем беспокоиться. . - person Neutrino; 18.11.2011

std::string::clear() определяется в терминах std::string::erase(), а std::string::erase() действительно должен переместить все символы после стертого блока. Так почему бы ему не вызвать для этого стандартную функцию? Если у вас есть выходные данные профилировщика, которые доказывают, что это узкое место, то, возможно, вы можете пожаловаться на это, но в остальном, честно говоря, я не вижу, чтобы это имело значение. (Логика, необходимая для избежания звонка, может в конечном итоге обойтись дороже, чем звонок.)

Кроме того, вы не проверяете результаты вызова getline перед их использованием. Ваш цикл должен быть примерно таким:

while ( std::getline( file, line ) ) {
    //  ...
}

И если вас так беспокоит производительность, создание подстроки (нового std::string) только для того, чтобы провести сравнение, намного дороже, чем вызов memmove_s. Что не так с чем-то вроде:

static std::string const target( "Whatever" );
if ( line.size() >= target.size()
        && std::equal( target.begin(), target().end(), line.being() ) ) {
    //  ...
}

Я считаю это наиболее идиоматическим способом определения того, начинается ли строка с определенного значения.

(Я могу добавить, что по опыту, reserve здесь тоже мало что вам нужно. После того, как вы прочтете пару строк в файле, ваша строка все равно не будет сильно расти, поэтому будет очень мало перераспределения после первой пары строк. Еще один случай преждевременной оптимизации?)

person James Kanze    schedule 17.11.2011
comment
Не думаю, что это преждевременно. Это не добавляет путаницы и дает преимущество в скорости. - person Mooing Duck; 17.11.2011
comment
@MooingDuck Я полагаю, вы говорите о reserve: он не оказывает заметного влияния на производительность и добавляет ненужную строку кода. - person James Kanze; 17.11.2011
comment
Я говорил о reserve да. Похоже, что у нас с вами разные определения преждевременной оптимизации, и это нормально. Это недостаточно важно для обсуждения. - person Mooing Duck; 17.11.2011
comment
Теперь я вижу очевидную вещь, которую я упустил, а именно то, что когда стирается только часть строки, стирание действительно должно переместить остальную часть материала вниз, однако, как указывает Бен Фойгт, в обычном случае стирания строки тогда целая строка представляет собой довольно большой объем кода, который выполняется вообще без всякой цели, и в этом общем случае проверка, позволяющая избежать вызова, не будет иметь ничего общего с затратами на выполнение всего этого бессмысленного кода. Спасибо за другие советы, все в порядке, я еще не закончил приводить в порядок свою петлю, пока меня не отвлекло то, что я обнаружил в стирании :) - person Neutrino; 18.11.2011
comment
По поводу резерва я склонен согласиться с тем, что технически это, вероятно, преждевременная оптимизация. Однако я думаю, что конкретный случай выбора разумного размера буфера перед входом в цикл является достаточно стандартным шаблоном, который, возможно, может считаться просто хорошей формой, а не оптимизацией как таковой. Возможно, это просто вопрос мнения. - person Neutrino; 18.11.2011
comment
@Neutrino: Скорее, когда конец строки не стирается, требуется ход. Всякий раз, когда строка усекается (до нулевой длины или нет), перемещение не требуется. И уже была проверка erase, исправление - чистая экономия без каких-либо недостатков. - person Ben Voigt; 18.11.2011
comment
Да, я понял это, я просто не мог четко это повторить, так как у меня закончились символы, поэтому я выразил это в терминах полного стирания в моем примере :) - person Neutrino; 18.11.2011
comment
@Neutrino Я не уверен, что вижу, где выполняется много кода. Вызов memmove_s с длиной 0 - это не то, что я бы назвал большим количеством кода. И добавление if вправо и влево для обработки особых случаев (даже если они очень часты) не обязательно улучшает ситуацию. (G ++ обрабатывает clear() примерно таким же образом, используя std::copy вместо memmove_s.) Было бы интересно изменить строковый класс так, чтобы clear просто устанавливал для поля длины значение 0 и ничего больше, и посмотреть, сильно ли это изменилось. в различных приложениях. - person James Kanze; 18.11.2011
comment
Если вы перейдете по ссылке в ветке Бена, парень из MS уже опубликовал метрики производительности фиксированной функции стирания. По-моему, неплохое улучшение. - person Neutrino; 18.11.2011
comment
@Neutrino Я не видел никаких мер относительно его влияния на реальный код. Не то чтобы это изменение не улучшило ситуацию, по крайней мере, для некоторых архитектур. Но я бы не стал считать это изменение исправлением ошибки, а скорее улучшением - Microsoft щедро считает это ошибкой. (Исторически, конечно, во многих реализациях использовался подсчет ссылок, а в g ++ он все еще используется. А с подсчетом ссылок оптимизация более сложна.) - person James Kanze; 18.11.2011

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

size_t not_found = std::string::npos;
std::istringstream buffer;

buffer << file.rdbuf();

std::string &data = buffer.str();

char const target[] = "\nWhatever";
size_t len = sizeof(target)-1;

for (size_t pos=0; not_found!=(pos=data.find(target, pos)); pos+=len)
{
    // process relevant line starting at contents[pos+1]
}
person Jerry Coffin    schedule 17.11.2011
comment
На самом деле я имел в виду перейти на еще более низкий уровень и использовать Win32 CreateFile для чтения всего файла в необработанный буфер памяти, и единственная причина, по которой я даже думал об этом, заключалась в том, что именно так все остальные операции ввода-вывода выполняются в приложении, которое я сейчас работает над :( В вашем примере, даже если вы берете ссылку на istringstream.str (), это все еще копия всей контролируемой последовательности, правда? В этом случае, поскольку вы скопировали весь файл, это действительно более эффективно, чем чтение файл построчно? - person Neutrino; 18.11.2011
comment
@Neutrino: Это может быть или не быть копией. По моему опыту, он обычно имеет лучшую производительность, чем построчное чтение. Возможно, вы захотите посмотреть предыдущий ответ для небольшого тестирования и более производительного кода. По крайней мере, если память не изменяет, кажется, что построчное чтение примерно с той же скоростью, что и используемые там итераторы. - person Jerry Coffin; 18.11.2011