处理字符串的最快方法

时间:2018-10-22 05:27:29

标签: c++ fwrite fread

我有一个文本文件,可以读取和处理2万行。在文本文件中,我想读取点坐标并分配给DirectX进行渲染。Snapshot of Text file

我使用了std :: ifstream,getline,stringstream来获取点坐标。生成win32程序后,开始运行,读取和将点坐标存储在数组中花费的时间太长。 (5分钟遍历20000行文本文件)。代码如下:

struct PointCoord { std::string PtName; float PtX = 0.0; float PtY = 0.0;}
PointCoord *PointPtr = NULL;
PointCoord pointcoord;

std::ifstream File_read(FileNameTXT);    
while (getline(File_read, TextHandler))
        {
            std::istringstream iss;
            std::string skip;
            if (TextHandler.find("  POINT ") != std::string::npos)
            {
                iss.str(TextHandler);
                std::string TempX, TempY;
                iss >> skip;
                iss >> pointcoord.PtName;                         

                //pointcoord pass value to PointCoord
                iss >> TempX;
                iss >> TempY;

                pointcoord.PtX = std::stof(TempX.c_str());
                pointcoord.PtY = std::stof(TempY.c_str());

                //dynamically store the points coordiantes
                if (PointPtr == NULL)
                {
                    PointPtr = new PointCoord[1];                     

                    //PointCoord pass value to PointPtr
                    PointPtr[0] = pointcoord;
                    Pt_Count++;
                }
                else
                {
                    PointCoord *Temp = PointPtr;
                    PointPtr = new PointCoord[Pt_Count + 1];

                    for (UINT i = 0; i < Pt_Count; i++)
                    {
                        PointPtr[i] = Temp[i];
                    }
                    PointPtr[Pt_Count] = pointcoord;
                    Pt_Count++;
                    delete[]Temp;
                }
            }//end of loading points
      }//end of getline

我还使用std :: fread在字符串缓冲区中一次读取整个文本文件,这是快速的(读取在几秒钟内完成)。然后使用类似代码的stringstream将点坐标存储在动态数组中,这也太慢了。

欢迎任何建议。非常感谢。

2 个答案:

答案 0 :(得分:11)

此代码中最令人讨厌的不是字符串解析;而是字符串解析。对于每个新读取的点,调整目标数组的大小。您读得越多,就越糟。最终,它成为O(n ^ 2)量级的复制操作。

要让您知道这有多糟,请考虑一下basic summation of n natural numbers,因为这就是您正在执行的对象构造,破坏和复制数量:

  

n(n + 1)/ 2 =(20000 * 20001)/ 2 = 200010000个对象已创建,复制和销毁

因此,字符串解析不是问题。超过2亿个对象的构造,销毁和复制,使被解析的20000行文本相形见


您无需执行任何操作。使用适当的容器,例如std::vector,并根据文件大小估算初始保留量。然后,只需生成点并将其移入容器即可。

执行此操作的示例,包括生成100000点(您要求的大小的5倍)的测试文件,如下所示:

代码

#include <iostream>
#include <fstream>
#include <sstream>
#include <vector>
#include <string>
#include <random>
#include <chrono>

struct Point
{
    std::string name;
    float x;
    float y;
};

std::vector<Point> readFile(std::string const& fname)
{
    std::vector<Point> res;

    std::ifstream inp(fname);
    if (inp.is_open())
    {
        // gather file size for a rough approximation of reserve
        inp.seekg(0, std::ios::end);
        auto flen = inp.tellg();
        inp.seekg(0, std::ios::beg);
        res.reserve(flen/40);

        std::string line;
        while (std::getline(inp, line))
        {
            auto pos = line.find("POINT");
            if (pos != std::string::npos)
            {
                std::istringstream iss(line.substr(pos+5));
                Point pt;
                if (iss >> pt.name >> pt.x >> pt.y)
                    res.emplace_back(std::move(pt));
            }
        }
    }

    return res;
}


int main()
{
    using namespace std::chrono;

    std::mt19937 prng(std::random_device{}());
    std::uniform_real_distribution<float> dist(-100.0, 100.0);

    // generate test data
    std::ofstream outf("testpoints.txt");
    for (int i=1; i<=100000; ++i)
        outf << "POINT  \"" << i << "\" " << dist(prng) << ' ' << dist(prng) << '\n';
    outf.close();

    // rough benchmark
    auto tp0 = steady_clock::now();
    auto v = readFile("testpoints.txt");
    auto tp1 = steady_clock::now();
    std::cout << "Read " << v.size() << " points in ";
    std::cout << duration_cast<milliseconds>(tp1-tp0).count() << "ms\n";
    v.clear();
}

输出

在2015年双核i7 MacBook Air笔记本电脑上运行时,发布模式版本会产生以下结果:

Read 100000 points in 164ms

一个可能更合适的容器:std::deque

最后,您真正需要的是一个容器,该容器允许在末端快速插入,同时在调整大小的过程中最小化(或消除)元素复制。当然,如上面的代码所示,针对std::vector设置储备金是这样做的一种方法。另一种选择是使用实际上专门用于最终插入的容器,同时仍然对缓存非常友好(不像std::vector那样完美,但肯定比链接列表之类的东西更好,后者对于插入非常有用,但是 dreadful 进行枚举)。

这正是std::deque会做的。上面的代码已更改为std::deque,使您可以消除保留猜测,而只需在序列末尾开始猛击节点,随着序列的增长,将自动添加更多页面:

代码

#include <iostream>
#include <fstream>
#include <sstream>
#include <deque>
#include <string>
#include <random>
#include <chrono>

struct Point
{
    std::string name;
    float x;
    float y;
};

std::deque<Point> readFile(std::string const& fname)
{
    std::deque<Point> res;

    std::ifstream inp(fname);
    if (inp.is_open())
    {
        std::string line;
        while (std::getline(inp, line))
        {
            auto pos = line.find("POINT");
            if (pos != std::string::npos)
            {
                std::istringstream iss(line.substr(pos+5));
                Point pt;
                if (iss >> pt.name >> pt.x >> pt.y)
                    res.emplace_back(std::move(pt));
            }
        }
    }

    return res;
}


int main()
{
    using namespace std::chrono;

    std::mt19937 prng(std::random_device{}());
    std::uniform_real_distribution<float> dist(-100.0, 100.0);

    // generate test data
    std::ofstream outf("testpoints.txt");
    for (int i=1; i<=100000; ++i)
        outf << "POINT  \"" << i << "\" " << dist(prng) << ' ' << dist(prng) << '\n';
    outf.close();

    // rough benchmark
    auto tp0 = steady_clock::now();
    auto v = readFile("testpoints.txt");
    auto tp1 = steady_clock::now();
    std::cout << "Read " << v.size() << " points in ";
    std::cout << duration_cast<milliseconds>(tp1-tp0).count() << "ms\n";
    v.clear();
}

输出

Read 100000 points in 160ms

如果您的需求需要连续的 序列,则可以使用std::vector方法。如果您只需要随机访问元素并想要快速插入末端,则std::deque可能更合适。考虑一下,然后选择最适合您的东西。


摘要

摆脱那种可怕的扩展算法。这是代码中的痛点。将其替换为几何尺寸调整算法,并从一开始就大致估算出所需元素的数量。或使用适合于最佳末端插入的容器。无论哪种方式,它都比您现在拥有的更好。

答案 1 :(得分:1)

如果您想进一步改进,可以做两件事。

  1. 您可以逐行读取文件,而不是逐行读取文件,而是使用基于指针的C样式代码将其拆分为几行,然后将每一行拆分为多个字段。就像您现在所做的那样,可以在一个迭代中完成两个步骤。这样,您可以编写除了名称之外不会复制字符串的代码。如果使用Visual C ++,请参见CAtlFileMapping<char>模板类。

  2. 标准的浮点解析器代码,std :: strtof中的一个,非常通用。如果您的文本为ASCII,并且数字以常规格式存储,例如-12.3456,即您没有INF或NAN,并且没有科学记数形式的数字(如-1.23E+1),则可以编写自己的数字strtof版本将比标准版本快2-3倍。