web-dev-qa-db-ja.com

C ++でスペースで区切られたフロートをすばやく解析する方法は?

何百万行ものファイルがあり、各行にはスペースで区切られた3つの浮動小数点があります。ファイルの読み取りには時間がかかるので、メモリマップファイルを使用してそれらを読み取ってみたところ、問題はIOの速度ではなく、解析。

私の現在の解析は、ストリーム(ファイルと呼ばれる)を取得し、次のことを行うことです

float x,y,z;
file >> x >> y >> z;

Stack Overflowの誰かがBoost.Spiritの使用を推奨しましたが、その使用方法を説明する簡単なチュートリアルは見つかりませんでした。

私は次のような行を解析するシンプルで効率的な方法を見つけようとしています:

"134.32 3545.87 3425"

本当に助かります。 strtokを使用して分割したかったのですが、文字列を浮動小数点数に変換する方法がわかりません。それが最善の方法かどうかはわかりません。

解決策がBoostになるかどうかは問題ではありません。それがこれまでで最も効率的なソリューションにならないかどうかは問題ではありませんが、速度を2倍にすることが可能だと確信しています。

前もって感謝します。

34
OopsUser

変換がボトルネックである場合(これはかなり可能です)、標準でさまざまな可能性を使用することから始める必要があります。論理的には、それらは非常に近いと予想されますが、実際には、常にそうとは限りません。

  • std::ifstreamの動作が遅すぎると既に判断しています。

  • メモリマップされたデータをstd::istringstreamに変換することは、ほぼ間違いなくnotが適切な解決策です。まず、すべてのデータをコピーする文字列を作成する必要があります。

  • コピーせずに(または非推奨のstd::istrstreamを使用せずに)独自のstreambufを書き込んでメモリから直接読み取ることは、問題が実際に変換である場合でも解決策になる可能性があります...変換ルーチン。

  • メモリマップストリームでいつでもfscanfまたはscanfを試すことができます。実装によっては、さまざまなistream実装よりも高速になる場合があります。

  • おそらくこれらのどれよりも速いのはstrtodを使うことです。このためにトークン化する必要はありません。strtodは先頭の空白('\n'を含む)をスキップし、読み取られていない最初の文字のアドレスを配置する出力パラメーターを持っています。終了条件は少しトリッキーです。ループはおそらく次のようになります。

 char *開始; // mmapされたデータを指すように設定します... 
 // '\ 0' 
 //データを追跡するように配置する必要もあります。これはおそらく
 //最も難しい問題です。
 char * end; 
 errno = 0; 
 double tmp = strtod(begin、&end); 
 while(errno == 0 && end!= begin){
 // tmpで何でもする... 
 begin = end; 
 tmp = strtod(begin、 &終わり );
    }

これらのどれも十分に高速でない場合は、実際のデータを考慮する必要があります。これにはおそらく何らかの追加の制約があります。つまり、より一般的なものよりも高速な変換ルーチンを作成できる可能性があります。例えばstrtodは固定と科学の両方を処理する必要があり、有効数字が17桁であっても100%正確でなければなりません。また、ロケール固有である必要があります。これらはすべて複雑さを増します。つまり、実行するコードが追加されます。ただし、注意してください。たとえ制限された入力セットであっても、効率的で正しい変換ルーチンを作成することは簡単ではありません。あなたは本当に自分が何をしているかを知る必要があります。

編集:

好奇心から、いくつかのテストを実行しました。上記の解決策に加えて、固定小数点のみを処理する(科学的ではない)シンプルなカスタムコンバーターを作成しました。小数点以下最大5桁で、小数点の前の値はintに収まる必要があります:

double
convert( char const* source, char const** endPtr )
{
    char* end;
    int left = strtol( source, &end, 10 );
    double results = left;
    if ( *end == '.' ) {
        char* start = end + 1;
        int right = strtol( start, &end, 10 );
        static double const fracMult[] 
            = { 0.0, 0.1, 0.01, 0.001, 0.0001, 0.00001 };
        results += right * fracMult[ end - start ];
    }
    if ( endPtr != nullptr ) {
        *endPtr = end;
    }
    return results;
}

(これを実際に使用する場合は、間違いなくいくつかのエラー処理を追加する必要があります。これは、私が生成したテストファイルを読み取るために、実験目的ですぐにノックアップされただけで、nothing以外です。)

インターフェースは、コーディングを単純化するために、strtodとまったく同じです。

私は2つの環境でベンチマークを実行しました(異なるマシン上にあるため、絶対値は関係ありません)。次の結果が得られました。

VC 11(/ O2)でコンパイルされたWindows 7の場合:

Testing Using fstream directly (5 iterations)...
    6.3528e+006 microseconds per iteration
Testing Using fscan directly (5 iterations)...
    685800 microseconds per iteration
Testing Using strtod (5 iterations)...
    597000 microseconds per iteration
Testing Using manual (5 iterations)...
    269600 microseconds per iteration

Linux 2.6.18で、g ++ 4.4.2(-O2、IIRC)を使用してコンパイルした場合:

Testing Using fstream directly (5 iterations)...
    784000 microseconds per iteration
Testing Using fscanf directly (5 iterations)...
    526000 microseconds per iteration
Testing Using strtod (5 iterations)...
    382000 microseconds per iteration
Testing Using strtof (5 iterations)...
    360000 microseconds per iteration
Testing Using manual (5 iterations)...
    186000 microseconds per iteration

すべての場合において、私は554000行を読み取っています。各行には、[0...10000)の範囲でランダムに生成された3つの浮動小数点があります。

最も顕著なのは、Windowsでのfstreamfscanの大きな違いです(およびfscanstrtodの違いは比較的小さいです)。 2つ目は、両方のプラットフォームで、単純なカスタム変換関数がどれだけ得られるかです。必要なエラー処理は少し遅くなりますが、その違いは依然として重要です。標準の変換ルーチンが行う多くの処理(科学形式、非常に小さい数、InfとNaN、i18nなど)を処理しないため、多少の改善が期待されましたが、それほど多くはありません。

18
James Kanze

更新

Spirit X3はテストに利用できるので、ベンチマークを更新しました。一方、私は Nonius を使用して統計的に正しいベンチマークを取得しました。

以下のすべてのチャートが利用可能です インタラクティブオンライン

ベンチマークCMakeプロジェクト+使用されるテストデータはgithubにあります: https://github.com/sehe/bench_float_parsing

enter image description here

概要:

スピリットパーサーは最速です。 C++ 14を使用できる場合は、実験版のSpirit X3を検討してください。

enter image description here

上記はメモリマップドファイルを使用した対策です。 IOストリームを使用すると、ボード全体で低速になります。

enter image description here

c/POSIX FILE*関数呼び出しを使用すると、scanfほど遅くはありません。

enter image description here


以下は古い答えの一部です


スピリットバージョンを実装し、他の提案された回答と比較してベンチマークを実行しました。

これが私の結果です。すべてのテストは同じ入力本体(input.txtの515Mb)で実行されます。正確な仕様については、以下を参照してください。


(秒単位の実時間、2回以上の実行の平均)

驚いたことに、Boost Spiritは最速で最もエレガントであることがわかりました。

  • エラーの処理/報告
  • +/- InfおよびNaNと変数の空白をサポート
  • (他のmmapの回答とは対照的に)入力の終わりを検出するのにまったく問題はありません。
  • いい感じ:

    bool ok = phrase_parse(f,l,               // source iterators
         (double_ > double_ > double_) % eol, // grammar
         blank,                               // skipper
         data);                               // output attribute
    

boost::spirit::istreambuf_iteratorの速度は言葉で言い表せないほど大幅に低下したことに注意してください(15秒以上)。これが役に立てば幸いです!

ベンチマークの詳細

すべての解析はstruct float3 { float x,y,z; }vectorに対して行われました。

を使用して入力ファイルを生成する

od -f -A none --width=12 /dev/urandom | head -n 11000000

これにより、次のようなデータを含む515Mbファイルが生成されます。

     -2627.0056   -1.967235e-12  -2.2784738e+33
  -1.0664798e-27  -4.6421956e-23   -6.917859e+20
  -1.1080849e+36   2.8909405e-33   1.7888695e-12
  -7.1663235e+33  -1.0840628e+36   1.5343362e-12
  -3.1773715e-17  -6.3655537e-22   -8.797282e+31
    9.781095e+19   1.7378472e-37        63825084
  -1.2139188e+09  -5.2464635e-05  -2.1235992e-38
   3.0109424e+08   5.3939846e+30  -6.6146894e-20

以下を使用してプログラムをコンパイルします。

g++ -std=c++0x -g -O3 -isystem -march=native test.cpp -o test -lboost_filesystem -lboost_iostreams

を使用して実時間を測定する

time ./test < input.txt 

環境:

  • Linuxデスクトップ4.2.0-42-generic#49-Ubuntu SMP x86_64
  • Intel(R)Core(TM)i7-3770K CPU @ 3.50GHz
  • 32GiB RAM

完全なコード

古いベンチマークの完全なコードは この投稿の編集履歴 にあり、最新バージョンは githubに です。

45
sehe

開始する前に、これがアプリケーションの遅い部分であることを確認し、改善を測定できるようにテストハーネスをその周辺に配置します。

boost::spirit私の考えでは、これはやり過ぎです。 fscanfをお試しください

FILE* f = fopen("yourfile");
if (NULL == f) {
   printf("Failed to open 'yourfile'");
   return;
}
float x,y,z;
int nItemsRead = fscanf(f,"%f %f %f\n", &x, &y, &z);
if (3 != nItemsRead) {
   printf("Oh dear, items aren't in the right format.\n");
   return;
}
13
Jeff Foster

私はこの関連する投稿 ifstreamを使用して浮動小数点数を読み取る または C++で文字列をトークン化する方法 特にC++ String Toolkit Libraryに関連する投稿をチェックします。私はC strtok、C++ストリーム、Boostトークナイザーを使用しましたが、使いやすくて使いやすいのはC++文字列ツールキットライブラリです。

2
DannyK

文字列処理で最も重要なルールの1つは、「一度に1文字ずつ読み取ります」だと思います。常にシンプルで、高速で、信頼性が高いと思います。

簡単なベンチマークプログラムを作成して、それがいかに単純かを示しました。私のテストによると、このコードはstrtodバージョンよりも40%高速に実行されます。

#include <iostream>
#include <sstream>
#include <iomanip>
#include <stdlib.h>
#include <math.h>
#include <time.h>
#include <sys/time.h>

using namespace std;

string test_generate(size_t n)
{
    srand((unsigned)time(0));
    double sum = 0.0;
    ostringstream os;
    os << std::fixed;
    for (size_t i=0; i<n; ++i)
    {
        unsigned u = Rand();
        int w = 0;
        if (u > UINT_MAX/2)
            w = - (u - UINT_MAX/2);
        else
            w = + (u - UINT_MAX/2);
        double f = w / 1000.0;
        sum += f;

        os << f;
        os << " ";
    }
    printf("generated %f\n", sum);
    return os.str();
}

void read_float_ss(const string& in)
{
    double sum = 0.0;
    const char* begin = in.c_str();
    char* end = NULL;
    errno = 0;
    double f = strtod( begin, &end );
    sum += f;

    while ( errno == 0 && end != begin )
    {
        begin = end;
        f = strtod( begin, &end );
        sum += f;
    }
    printf("scanned %f\n", sum);
}

double scan_float(const char* str, size_t& off, size_t len)
{
    static const double bases[13] = {
        0.0, 10.0, 100.0, 1000.0, 10000.0,
        100000.0, 1000000.0, 10000000.0, 100000000.0,
        1000000000.0, 10000000000.0, 100000000000.0, 1000000000000.0,
    };

    bool begin = false;
    bool fail = false;
    bool minus = false;
    int pfrac = 0;

    double dec = 0.0;
    double frac = 0.0;
    for (; !fail && off<len; ++off)
    {
        char c = str[off];
        if (c == '+')
        {
            if (!begin)
                begin = true;
            else
                fail = true;
        }
        else if (c == '-')
        {
            if (!begin)
                begin = true;
            else
                fail = true;
            minus = true;
        }
        else if (c == '.')
        {
            if (!begin)
                begin = true;
            else if (pfrac)
                fail = true;
            pfrac = 1;
        }
        else if (c >= '0' && c <= '9')
        {
            if (!begin)
                begin = true;
            if (pfrac == 0)
            {
                dec *= 10;
                dec += c - '0';
            }
            else if (pfrac < 13)
            {
                frac += (c - '0') / bases[pfrac];
                ++pfrac;
            }
        }
        else
        {
            break;
        }
    }

    if (!fail)
    {
        double f = dec + frac;
        if (minus)
            f = -f;
        return f;
    }

    return 0.0;
}

void read_float_direct(const string& in)
{
    double sum = 0.0;
    size_t len = in.length();
    const char* str = in.c_str();
    for (size_t i=0; i<len; ++i)
    {
        double f = scan_float(str, i, len);
        sum += f;
    }
    printf("scanned %f\n", sum);
}

int main()
{
    const int n = 1000000;
    printf("count = %d\n", n);

    string in = test_generate(n);    
    {
        struct timeval t1;
        gettimeofday(&t1, 0);
        printf("scan start\n");

        read_float_ss(in);

        struct timeval t2;
        gettimeofday(&t2, 0);
        double elapsed = (t2.tv_sec - t1.tv_sec) * 1000000.0;
        elapsed += (t2.tv_usec - t1.tv_usec) / 1000.0;
        printf("elapsed %.2fms\n", elapsed);
    }

    {
        struct timeval t1;
        gettimeofday(&t1, 0);
        printf("scan start\n");

        read_float_direct(in);

        struct timeval t2;
        gettimeofday(&t2, 0);
        double elapsed = (t2.tv_sec - t1.tv_sec) * 1000000.0;
        elapsed += (t2.tv_usec - t1.tv_usec) / 1000.0;
        printf("elapsed %.2fms\n", elapsed);
    }
    return 0;
}

以下はi7 Mac Book Pro(XCode 4.6でコンパイル)からのコンソール出力です。

count = 1000000
generated -1073202156466.638184
scan start
scanned -1073202156466.638184
elapsed 83.34ms
scan start
scanned -1073202156466.638184
elapsed 53.50ms
0
9dan

[〜#〜] edit [〜#〜]:crack_atofがどのようにも検証されないことが心配な場合は、下のコメントを参照してください-

Nice C++ 17 from_chars()ソリューションはMSVCでのみ機能するため(clangやgccではなく)、より完全な(標準ではありませんが)高速文字列から二重ルーチンへの変換を次に示します。

会うcrack_atof

https://Gist.github.com/oschonrock/a410d4bec6ec1ccc5a3009f0907b3d15

私の仕事ではなく、少しリファクタリングしただけです。そして、署名を変更しました。コードは非常に理解しやすく、なぜ高速なのかは明らかです。そして、それは非常に高速です。ここのベンチマークを参照してください:

https://www.codeproject.com/Articles/1130262/Cplusplus-string-view-Conversion-to-Integral-Types

私は、3フロートの11,0​​00,000行で実行しました(csvで15桁の精度、重要です!)。私の古い第2世代Core i7 2600では、1.327秒で動作しました。コンパイラclang V8.0.0 -O2(Kubuntu 19.04)。

以下の完全なコード。 mmapを使用しています。crack_atofのおかげで、str-> floatが唯一のボトルネックではなくなったためです。マップのRAIIリリースを確実にするために、mmapをクラスにラップしました。


#include <iomanip>
#include <iostream>

// for mmap:
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>

class MemoryMappedFile {
public:
  MemoryMappedFile(const char* filename) {
    int fd = open(filename, O_RDONLY);
    if (fd == -1) throw std::logic_error("MemoryMappedFile: couldn't open file.");

    // obtain file size
    struct stat sb;
    if (fstat(fd, &sb) == -1) throw std::logic_error("MemoryMappedFile: cannot stat file size");
    m_filesize = sb.st_size;

    m_map = static_cast<const char*>(mmap(NULL, m_filesize, PROT_READ, MAP_PRIVATE, fd, 0u));
    if (m_map == MAP_FAILED) throw std::logic_error("MemoryMappedFile: cannot map file");
  }

  ~MemoryMappedFile() {
    if (munmap(static_cast<void*>(const_cast<char*>(m_map)), m_filesize) == -1)
      std::cerr << "Warnng: MemoryMappedFile: error in destructor during `munmap()`\n";
  }

  const char* start() const { return m_map; }
  const char* end() const { return m_map + m_filesize; }

private:
  size_t m_filesize = 0;
  const char* m_map = nullptr;
};

// high speed str -> double parser
double pow10(int n) {
  double ret = 1.0;
  double r   = 10.0;
  if (n < 0) {
    n = -n;
    r = 0.1;
  }

  while (n) {
    if (n & 1) {
      ret *= r;
    }
    r *= r;
    n >>= 1;
  }
  return ret;
}

double crack_atof(const char* start, const char* const end) {
  if (!start || !end || end <= start) {
    return 0;
  }

  int sign         = 1;
  double int_part  = 0.0;
  double frac_part = 0.0;
  bool has_frac    = false;
  bool has_exp     = false;

  // +/- sign
  if (*start == '-') {
    ++start;
    sign = -1;
  } else if (*start == '+') {
    ++start;
  }

  while (start != end) {
    if (*start >= '0' && *start <= '9') {
      int_part = int_part * 10 + (*start - '0');
    } else if (*start == '.') {
      has_frac = true;
      ++start;
      break;
    } else if (*start == 'e') {
      has_exp = true;
      ++start;
      break;
    } else {
      return sign * int_part;
    }
    ++start;
  }

  if (has_frac) {
    double frac_exp = 0.1;

    while (start != end) {
      if (*start >= '0' && *start <= '9') {
        frac_part += frac_exp * (*start - '0');
        frac_exp *= 0.1;
      } else if (*start == 'e') {
        has_exp = true;
        ++start;
        break;
      } else {
        return sign * (int_part + frac_part);
      }
      ++start;
    }
  }

  // parsing exponent part
  double exp_part = 1.0;
  if (start != end && has_exp) {
    int exp_sign = 1;
    if (*start == '-') {
      exp_sign = -1;
      ++start;
    } else if (*start == '+') {
      ++start;
    }

    int e = 0;
    while (start != end && *start >= '0' && *start <= '9') {
      e = e * 10 + *start - '0';
      ++start;
    }

    exp_part = pow10(exp_sign * e);
  }

  return sign * (int_part + frac_part) * exp_part;
}

int main() {
  MemoryMappedFile map  = MemoryMappedFile("FloatDataset.csv");
  const char* curr      = map.start();
  const char* start     = map.start();
  const char* const end = map.end();

  uintmax_t lines_n = 0;
  int cnt              = 0;
  double sum           = 0.0;
  while (curr && curr != end) {
    if (*curr == ',' || *curr == '\n') {
      // std::string fieldstr(start, curr);
      // double field = std::stod(fieldstr);
      // m_numLines = 11000000 cnt=33000000 sum=16498294753551.9
      // real 5.998s

      double field = crack_atof(start, curr);
      // m_numLines = 11000000 cnt=33000000 sum=16498294753551.9
      // real 1.327s

      sum += field;
      ++cnt;
      if (*curr == '\n') lines_n++;
      curr++;
      start = curr;
    } else {
      ++curr;
    }
  }

  std::cout << std::setprecision(15) << "m_numLines = " << lines_n << " cnt=" << cnt
            << " sum=" << sum << "\n";
}

Github Gistにもコードを記述します。

https://Gist.github.com/oschonrock/67fc870ba067ebf0f369897a9d52c2dd

0

cを使用することが最速のソリューションになるでしょう。 strtokを使用してトークンに分割し、次に strtof を使用して浮動小数点に変換します。または、正確な形式がわかっている場合は、fscanfを使用します。

0
log0

重要な解決策は、問題により多くのコアを投入し、複数のスレッドを生成することです。ボトルネックが単なるCPUである場合、2つのスレッド(マルチコアCPU上)を生成することにより、実行時間を半分にすることができます。

その他のヒント:

  • ブーストや標準などのライブラリから関数を解析しないようにしてください。それらはエラーチェック条件で肥大化し、処理時間の多くはこれらのチェックに費やされます。数回の変換では問題ありませんが、何百万もの値を処理する場合は無残に失敗します。データが適切にフォーマットされていることがすでにわかっている場合は、データ変換のみを行うカスタム最適化C関数を作成(または検索)できます

  • ファイルのチャンクをロードし、そこで変換を行う大きなメモリバッファー(10 Mバイトとしましょう)を使用します。

  • divide et impera:問題をより簡単な問題に分割します。ファイルを前処理し、1行を1フロートにし、各行を「。」で分割します。文字と浮動小数点数の代わりに整数を変換し、2つの整数をマージして浮動小数点数を作成します

0