удвоить в строку без экспоненциальной записи или завершающих нулей, эффективно

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

    static std::string dbl2str(double d)
    {
        std::stringstream ss;
        ss << std::fixed << std::setprecision(10) << d;              //convert double to string w fixed notation, hi precision
        std::string s = ss.str();                                    //output to std::string
        s.erase(s.find_last_not_of('0') + 1, std::string::npos);     //remove trailing 000s    (123.1200 => 123.12,  123.000 => 123.)
        return (s[s.size()-1] == '.') ? s.substr(0, s.size()-1) : s; //remove dangling decimal (123. => 123)
    }

person tpascale    schedule 01.03.2013    source источник
comment
Название кажется неправильным, должно быть двойное для строки?   -  person hyde    schedule 02.03.2013
comment
упс - название задом наперёд. . . конечно, это двойная строка   -  person tpascale    schedule 02.03.2013


Ответы (3)


Прежде чем начать, проверьте, не тратится ли значительное время на эту функцию. Сделайте это путем измерения либо с помощью профилировщика, либо иным образом. Зная, что вы называете это миллион раз, это очень хорошо, но если окажется, что ваша программа по-прежнему тратит только 1% своего времени на эту функцию, то никакие ваши действия здесь не могут улучшить производительность вашей программы более чем на 1%. Если бы это было так, ответом на ваш вопрос было бы «для ваших целей нет, эту функцию нельзя сделать значительно более эффективной, и вы тратите свое время, если попытаетесь».

Во-первых, избегайте s.substr(0, s.size()-1). Это копирует большую часть строки и, что делает вашу функцию неприемлемой для NRVO, поэтому я думаю, что обычно вы получите копию по возвращении. Итак, первое изменение, которое я бы сделал, это заменить последнюю строку на:

if(s[s.size()-1] == '.') {
    s.erase(s.end()-1);
}
return s;

Но если производительность является серьезной проблемой, то вот как я бы это сделал. Я не обещаю, что это будет максимально быстро, но это позволит избежать некоторых проблем с ненужными выделениями памяти и копированием. Любой подход, включающий stringstream, потребует копирования из потока строк в результат, поэтому нам нужна более низкоуровневая операция, snprintf.

static std::string dbl2str(double d)
{
    size_t len = std::snprintf(0, 0, "%.10f", d);
    std::string s(len+1, 0);
    // technically non-portable, see below
    std::snprintf(&s[0], len+1, "%.10f", d);
    // remove nul terminator
    s.pop_back();
    // remove trailing zeros
    s.erase(s.find_last_not_of('0') + 1, std::string::npos);
    // remove trailing point
    if(s.back() == '.') {
        s.pop_back();
    }
    return s;
}

Второй вызов snprintf предполагает, что std::string использует непрерывное хранилище. Это гарантируется в C++11. Это не гарантируется в C++03, но верно для всех активно поддерживаемых реализаций std::string, известных комитету C++. Если производительность действительно важна, то я думаю, что разумно сделать это непереносимое предположение, поскольку запись непосредственно в строку сохраняет копирование в строку позже.

s.pop_back() — это способ C++11 сказать s.erase(s.end()-1), а s.back() — это s[s.size()-1]

Для другого возможного улучшения вы можете избавиться от первого вызова snprintf и вместо этого изменить размер s на некоторое значение, например std::numeric_limits<double>::max_exponent10 + 14 (в основном, длину, которая нужна -DBL_MAX). Проблема в том, что при этом выделяется и обнуляется гораздо больше памяти, чем обычно требуется (322 байта для двойника IEEE). Моя интуиция подсказывает, что это будет медленнее, чем первый вызов snprintf, не говоря уже о расточительном использовании памяти в случае, когда возвращаемое строковое значение какое-то время сохраняется вызывающей стороной. Но вы всегда можете проверить это.

В качестве альтернативы, std::max((int)std::log10(d), 0) + 14 вычисляет достаточно точную верхнюю границу необходимого размера и может быть быстрее, чем snprintf, может вычислить его точно.

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

void append_dbl2str(std::string &s, double d) {
    size_t len = std::snprintf(0, 0, "%.10f", d);
    size_t oldsize = s.size();
    s.resize(oldsize + len + 1);
    // technically non-portable
    std::snprintf(&s[oldsize], len+1, "%.10f", d);
    // remove nul terminator
    s.pop_back();
    // remove trailing zeros
    s.erase(s.find_last_not_of('0') + 1, std::string::npos);
    // remove trailing point
    if(s.back() == '.') {
        s.pop_back();
    }
}

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

person Steve Jessop    schedule 01.03.2013

Эффективно с точки зрения скорости или краткости?

char buf[64];
sprintf(buf, "%-.*G", 16, 1.0);
cout << buf << endl;

Отображает «1». Форматирует до 16 значащих цифр без нулей в конце перед возвратом к экспоненциальному представлению.

person BSalita    schedule 22.12.2013
comment
- не является строго необходимым (оно оправдывает себя) - person Benjamin Herreid; 02.12.2014

  • используйте snprintf и массив char вместо stringstream и string
  • передать указатель на буфер char в dbl2str, в который он печатает (чтобы избежать вызова конструктора копирования string при возврате). Соберите строку для печати в буфере символов (или преобразуйте буфер символов при вызове в строку или добавьте его в существующую строку)
  • объявить функцию inline в заголовочном файле

    #include <cstdio>
    inline void dbl2str(char *buffer, int bufsize, double d)
    {
      /** the caller must make sure that there is enough memory allocated for buffer */
      int len = snprintf(buffer, bufsize, "%lf", d);
    
      /* len is the number of characters put into the buffer excluding the trailing \0
         so buffer[len] is the \0 and buffer[len-1] is the last 'visible' character */
    
      while (len >= 1 && buffer[len-1] == '0')
        --len;
    
      /* terminate the string where the last '0' character was or overwrite the existing
         0 if there was no '0' */
      buffer[len] = 0;
    
      /* check for a trailing decimal point */
      if (len >= 1 && buffer[len-1] == '.')
        buffer[len-1] = 0;
    }
    
person Andre Holzner    schedule 01.03.2013
comment
Ключевое слово inline напрямую не влияет на оптимизацию путем встраивания, это инструкция для линковщика, что этот символ может появляться много раз в линковке и это не ошибка. Функция уже является статической, о которой идет речь. - person hyde; 02.03.2013