从文件加载(大)std::vector<std::vector<float>> 的更快方法

Faster way of loading (big) std::vector<std::vector<float>> from file

我已经实现了一种将 std::vector 向量保存到文件并使用此代码读取它们的方法(在 Whosebug 上找到):

节省:

void saveData(std::string path)
{
    std::ofstream FILE(path, std::ios::out | std::ofstream::binary);

    // Store size of the outer vector
    int s1 = RecordData.size();
    FILE.write(reinterpret_cast<const char*>(&s1), sizeof(s1));

    // Now write each vector one by one
    for (auto& v : RecordData) {
        // Store its size
        int size = v.size();
        FILE.write(reinterpret_cast<const char*>(&size), sizeof(size));

        // Store its contents
        FILE.write(reinterpret_cast<const char*>(&v[0]), v.size() * sizeof(float));
    }
    FILE.close();
}

阅读中:

void loadData(std::string path)
{
    std::ifstream FILE(path, std::ios::in | std::ifstream::binary);

    if (RecordData.size() > 0) // Clear data
    {
        for (int n = 0; n < RecordData.size(); n++)
            RecordData[n].clear();
        RecordData.clear();
    }

    int size = 0;
    FILE.read(reinterpret_cast<char*>(&size), sizeof(size));
    RecordData.resize(size);
    for (int n = 0; n < size; ++n) {
        int size2 = 0;
        FILE.read(reinterpret_cast<char*>(&size2), sizeof(size2));
        float f;
        //RecordData[n].reserve(size2); // This doesn't make a difference in speed
        for (int k = 0; k < size2; ++k) {
            FILE.read(reinterpret_cast<char*>(&f), sizeof(f));
            RecordData[n].push_back(f);
        }
    }
}

这非常有效,但加载大型数据集(980MB,内部向量大小为 32000,其中 1600 个)需要大约 7-8 秒(与保存相比,保存时间不到 1 秒)。因为我可以看到 Visual Studio 中的内存使用量在加载过程中缓慢上升,所以我猜应该是大量内存分配。不过,注释掉的行 RecordData[n].resize(size2); 没有任何区别。

谁能给我一个更快的加载此类数据的方法?我的第一次尝试是将所有数据放在一个大的 std::vector<float> 中,但由于某种原因似乎会产生某种溢出(这不应该发生,因为 sizeof(int) = 4,所以大约 40 亿应该足够了对于索引变量(std::vector 在内部使用其他东西吗?)。拥有 std::vector<std::vector<float>> 的数据结构也非常好。将来我将不得不处理更大的数据集(尽管我可能会使用 <short> 来节省内存并将其作为定点数处理),因此加载速度将更加重要......

编辑:

我应该指出,内部向量的 32000 和外部向量的 1600 只是一个示例。两者都可以变化。我想,我必须保存一个“索引向量”作为第一个内部向量来声明其余的项目数(就像我在评论中说的那样:我是第一次 file-reader/-writer 和避风港我用 std::vector 的时间超过一两周,所以我不确定)。我会在以后的编辑中查看块读取和 post 结果...

编辑2:

所以,这是 perivesta 的版本(谢谢你)。我所做的唯一更改是丢弃 RV& RecordData 因为这对我来说是一个全局变量。

奇怪的是,对于 perivesta,对于 980 GB 的文件,这只会使我的加载时间从 ~7000ms 减少到 ~1500ms,而对于 perivesta 的 2GB 文件,加载时间不会从 7429ms 减少到 644 ms(奇怪,不同系统上的速度有何不同 ;-) )

void loadData2(std::string path)
{
    std::ifstream FILE(path, std::ios::in | std::ifstream::binary);

    if (RecordData.size() > 0) // Clear data
    {
        for (int n = 0; n < RecordData.size(); n++)
            RecordData[n].clear();
        RecordData.clear();
    }

    int size = 0;
    FILE.read(reinterpret_cast<char*>(&size), sizeof(size));
    RecordData.resize(size);
    for (auto& v : RecordData) {
        // load its size
        int size2 = 0;
        FILE.read(reinterpret_cast<char*>(&size2), sizeof(size2));
        v.resize(size2);

        // load its contents
        FILE.read(reinterpret_cast<char*>(&v[0]), v.size() * sizeof(float));
    }
}
//RecordData[n].resize(size2); // This doesn't make a difference in speed

如果您使用这一行(同时不更改其余代码),代码会变慢,而不是变快!

resize 更改向量的大小,然后将更多元素推入它,导致向量的大小是您实际需要的两倍。

我猜你想要的是 reservereserve 只分配容量而不改变向量的大小,然后推送元素可以预期更快,因为内存只分配一次。

或者使用 resize,然后分配给已经存在的元素。

首先,由于您预先知道元素的数量,因此您应该在向量中 reserve space 以防止在向量增长时进行不必要的重新分配。其次,所有这些 push_back 可能会让您付出代价。该功能确实有一些开销。第三,正如 Alan 所说,一次读取整个文件不可能有什么坏处,如果您先调整(而不是保留)向量的大小,就可以做到这一点。

所以,综上所述,我会这样做(一旦您将数据大小读入 size2):

RecordData.resize(size2);                // both reserves and allocates space for size2 items
FILE.read(reinterpret_cast<char*>(RecordData.data()), size2 * sizeof(float));

我认为这是最优的。

不幸的是,在这种情况下,IMO,当您调用 resize 时,std::vector 坚持 zero-initialising 所有 size2 元素,因为您将立即覆盖他们,但我不知道有什么容易预防的方法。您需要使用自定义分配器,这可能不值得付出努力。

这是 Alan Birtles 的评论的实现:阅读时,通过一个 FILE.read 调用而不是许多单独的调用来读取内部向量。这大大减少了我系统上的时间:

这些是 2GB 文件的结果:

Writing    took 2283 ms
Reading v1 took 7429 ms
Reading v2 took 644 ms

这里是产生这个输出的代码:

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

using RV = std::vector<std::vector<float>>;

void saveData(std::string path, const RV& RecordData)
{
    std::ofstream FILE(path, std::ios::out | std::ofstream::binary);

    // Store size of the outer vector
    int s1 = RecordData.size();
    FILE.write(reinterpret_cast<const char*>(&s1), sizeof(s1));

    // Now write each vector one by one
    for (auto& v : RecordData) {
        // Store its size
        int size = v.size();
        FILE.write(reinterpret_cast<const char*>(&size), sizeof(size));

        // Store its contents
        FILE.write(reinterpret_cast<const char*>(&v[0]), v.size() * sizeof(float));
    }
    FILE.close();
}

//original version for comparison
void loadData1(std::string path, RV& RecordData)
{
    std::ifstream FILE(path, std::ios::in | std::ifstream::binary);

    if (RecordData.size() > 0) // Clear data
    {
        for (int n = 0; n < RecordData.size(); n++)
            RecordData[n].clear();
        RecordData.clear();
    }

    int size = 0;
    FILE.read(reinterpret_cast<char*>(&size), sizeof(size));
    RecordData.resize(size);
    for (int n = 0; n < size; ++n) {
        int size2 = 0;
        FILE.read(reinterpret_cast<char*>(&size2), sizeof(size2));
        float f;
        //RecordData[n].resize(size2); // This doesn't make a difference in speed
        for (int k = 0; k < size2; ++k) {
            FILE.read(reinterpret_cast<char*>(&f), sizeof(f));
            RecordData[n].push_back(f);
        }
    }
}

//my version
void loadData2(std::string path, RV& RecordData)
{
    std::ifstream FILE(path, std::ios::in | std::ifstream::binary);

    if (RecordData.size() > 0) // Clear data
    {
        for (int n = 0; n < RecordData.size(); n++)
            RecordData[n].clear();
        RecordData.clear();
    }

    int size = 0;
    FILE.read(reinterpret_cast<char*>(&size), sizeof(size));
    RecordData.resize(size);
    for (auto& v : RecordData) {
        // load its size
        int size2 = 0;
        FILE.read(reinterpret_cast<char*>(&size2), sizeof(size2));
        v.resize(size2);

        // load its contents
        FILE.read(reinterpret_cast<char*>(&v[0]), v.size() * sizeof(float));
    }
}

int main()
{
    using namespace std::chrono;
    const std::string filepath = "./vecdata";
    const std::size_t sizeOuter = 16000;
    const std::size_t sizeInner = 32000;
    RV vecSource;
    RV vecLoad1;
    RV vecLoad2;

    const auto tGen1 = steady_clock::now();
    std::cout << "generating random numbers..." << std::flush;
    std::random_device dev;
    std::mt19937 rng(dev());
    std::uniform_real_distribution<float> dis;
    for(int i = 0; i < sizeOuter; ++i)
    {
        RV::value_type inner;
        for(int k = 0; k < sizeInner; ++k)
        {
            inner.push_back(dis(rng));
        }
        vecSource.push_back(inner);
    }
    const auto tGen2 = steady_clock::now();

    std::cout << "done\nSaving..." << std::flush;
    const auto tSave1 = steady_clock::now();
    saveData(filepath, vecSource);
    const auto tSave2 = steady_clock::now();

    std::cout << "done\nReading v1..." << std::flush;
    const auto tLoadA1 = steady_clock::now();
    loadData1(filepath, vecLoad1);
    const auto tLoadA2 = steady_clock::now();
    std::cout << "verifying..." << std::flush;
    if(vecSource != vecLoad1) std::cout << "FAILED! ...";

    std::cout << "done\nReading v2..." << std::flush;
    const auto tLoadB1 = steady_clock::now();
    loadData2(filepath, vecLoad2);
    const auto tLoadB2 = steady_clock::now();
    std::cout << "verifying..." << std::flush;
    if(vecSource != vecLoad2) std::cout << "FAILED! ...";


    std::cout << "done\nResults:\n" <<
        "Generating took " << duration_cast<milliseconds>(tGen2 - tGen1).count() << " ms\n" <<
        "Writing    took " << duration_cast<milliseconds>(tSave2 - tSave1).count() << " ms\n" <<
        "Reading v1 took " << duration_cast<milliseconds>(tLoadA2 - tLoadA1).count() << " ms\n" <<
        "Reading v2 took " << duration_cast<milliseconds>(tLoadB2 - tLoadB1).count() << " ms\n" <<
        std::flush;
}