在 C++ 中初始化非常大的向量

Posted

技术标签:

【中文标题】在 C++ 中初始化非常大的向量【英文标题】:Initialization of very big vector in C++ 【发布时间】:2016-09-16 10:57:33 【问题描述】:

我在 python 中创建了非常大的 O(10M) 浮点列表。我想在我的 C++ 项目中使用这个查找表。将此数组从 python 传输到 C++ 的最简单和最有效的方法是什么。

我的第一个想法是生成c++函数,负责对这么长的向量进行初始化,然后编译。 python代码如上所示:

def generate_initLookupTable_function():
    numbers_per_row = 100
    function_body = """
#include "PatBBDTSeedClassifier.h"

std::vector<double> PatBBDTSeedClassifier::initLookupTable()

   std::vector<double> indicesVector =
    """
    row_nb = 1
    for bin_value in classifier._lookup_table[:,0]:
        function_body += "\t" + str(bin_value) +" , "
        if (row_nb % numbers_per_row) == 0:
            function_body += "\n"
        row_nb += 1

    function_body += """\n ;
return indicesVector;

    """
    return function_body

输出文件的大小为 500 MB。并且无法编译(编译因 gcc 崩溃而终止):

../src/PatBBDTSeedClassifier_lookupTable.cpp
lcg-g++-4.9.3: internal compiler error: Killed (program cc1plus)

0x409edc execute
../../gcc-4.9.3/gcc/gcc.c:2854
Please submit a full bug report,
with preprocessed source if appropriate.
Please include the complete backtrace with any bug report.

另一个想法是将python数组存储到二进制文件中,然后用C++读取。但这很棘手。我无法正确阅读它。 我使用这样简单的命令生成表:

file = open("models/BBDT_lookuptable.dat", 'wb')
table = numpy.array(classifier._lookup_table[:,0])
table.tofile(file)
file.close()

你能告诉我我该怎么做吗?我用谷歌搜索,我找不到足够的答案。

你知道我该如何处理这么大的数组吗?

我应该给你更详细的问题描述。 我使用 python 来训练 ML (sklearn) 分类器,然后我想在 C++ 中部署它。 Doe to time 问题(执行速度是我研究的关键部分)我使用bonsai boosted decision trees 的想法。在这种方法中,您将 BDT 转移到查找表中。

【问题讨论】:

您是否有其他进程需要使用来自 Python 进程的数据?还是 C++“程序”应该是 Python 中使用的模块?您能否详细说明您试图通过“传输”数据来解决的潜在问题? 您能透露一些生成的 C++ 代码以便更好地理解问题吗? “另一个想法是将python数组存储到二进制文件中,然后用C++读取它。但这很棘手。我无法正确读取它。”。不幸的是,这是您唯一的声音选项 :) 等等,不:您可以生成 asm 而不是 C 并将 asm 代码链接到您的 C 代码。 出于好奇:你为什么不把它放在一个大的压缩二进制文件中呢?一定有一些很好的理由,因为它可以为您省去麻烦并且可能加载更快(试试Zstd)。 问题是我不知道如何从这样的文件中抓取表格。 【参考方案1】:

如果您使用的是 GNU 工具,直接使用 objcopy 来实现 Jean-Francois 所建议的功能相当容易;结合写二进制数组的PM2Ring的python脚本,可以执行:

objcopy -I binary test.data -B i386:x86-64 -O elf64-x86-64 testdata.o

(根据您的实际处理器架构,您可能需要进行调整)。该命令将使用以下符号创建一个名为 testdata.o 的新对象:

0000000000000100 D _binary_test_data_end
0000000000000100 A _binary_test_data_size
0000000000000000 D _binary_test_data_start

所有这些符号在链接程序中都将作为带有 C 链接的符号可见。 size 本身不能使用(它也将转换为地址),但可以使用 *start*end。这是一个最小的 C++ 程序:

#include <iostream>

extern "C" double _binary_test_data_start[];
extern "C" double _binary_test_data_end[0];

int main(void) 
    double *d = _binary_test_data_start;
    const double *end = _binary_test_data_end;

    std::cout << (end - d) << " doubles in total" << std::endl;
    while (d < end) 
        std::cout << *d++ << std::endl;
    

_binary_test_data_end 实际上将刚好超过数组 _binary_test_data_start 中的最后一个元素。

编译 + 将此程序与 g++ test.cc testdata.o -o program 链接(使用上面 objcopy 中的 testdata.o)。

输出(cout 默认情况下似乎会尴尬地截断小数):

% ./a.out 
32 doubles in total
0
0.0625
0.125
0.1875
0.25
0.3125
0.375
0.4375
0.5
0.5625
0.625
0.6875
0.75
0.8125
0.875
0.9375
1
1.0625
1.125
1.1875
1.25
1.3125
1.375
1.4375
1.5
1.5625
1.625
1.6875
1.75
1.8125
1.875
1.9375

你也可以很容易地将这些值分配到一个向量中; std::vector&lt;double&gt; 接受 2 个迭代器,其中第一个指向第一个元素,第二个指向后面的一个;您可以在这里使用数组,因为它们会衰减为指针,并且指针可以用作迭代器:

std::vector<double> vec(_binary_test_data_start, _binary_test_data_end);

但是,对于大数组,这只是不必要的复制。此外,仅使用 C 数组还有一个额外的好处,那就是它延迟加载; ELF 可执行文件不会读入内存,但会根据需要进行分页;二进制数组仅在被访问时才从文件加载到 RAM 中。

【讨论】:

【参考方案2】:

这是一个简单的示例,说明如何将 Python 浮点数据写入二进制文件,以及如何在 C 中读取该数据。为了对数据进行编码,我们使用了struct 模块。

savefloat.py

​​>
#!/usr/bin/env python3
from struct import pack

# The float data to save
table = [i / 16.0 for i in range(32)]

# Dump the table to stdout
for i, v in enumerate(table):
    print('%d: %f' % (i, v))

# Save the data to a binary file
fname = 'test.data'
with open(fname, 'wb') as f:
    for u in table:
        # Pack doubles as little-endian 
        f.write(pack(b'<d', u))    

输出

0: 0.000000
1: 0.062500
2: 0.125000
3: 0.187500
4: 0.250000
5: 0.312500
6: 0.375000
7: 0.437500
8: 0.500000
9: 0.562500
10: 0.625000
11: 0.687500
12: 0.750000
13: 0.812500
14: 0.875000
15: 0.937500
16: 1.000000
17: 1.062500
18: 1.125000
19: 1.187500
20: 1.250000
21: 1.312500
22: 1.375000
23: 1.437500
24: 1.500000
25: 1.562500
26: 1.625000
27: 1.687500
28: 1.750000
29: 1.812500
30: 1.875000
31: 1.937500

loadfloat.c

/* Read floats from a binary file & dump to stdout */

#include <stdlib.h>
#include <stdio.h>

#define FILENAME "test.data"
#define DATALEN 32

int main(void)

    FILE *infile;
    double data[DATALEN];
    int i, n;

    if(!(infile = fopen(FILENAME, "rb")))
        exit(EXIT_FAILURE);

    n = fread(data, sizeof(double), DATALEN, infile);
    fclose(infile);

    for(i=0; i<n; i++)
        printf("%d: %f\n", i, data[i]);

    return 0;

上面的 C 代码产生与 savefloat.py 相同的输出。

【讨论】:

谢谢!我一直在尝试用 C++ 实现这个功能。但正如你所展示的,C 更容易。 @user1877600:不用担心。我确信在 C++ 中读取浮点数据也很简单,如果您真的需要它,但我从未学过 C++。通过以块(例如 64 kB)写入数据可以使 Python 代码更快,但这可能不是必需的,因为现代操作系统在缓冲磁盘操作方面非常有效。 @user1877600:我刚刚修改了 C 代码,使其更加健壮。 对此的 C++ 解决方案与 C 解决方案几乎相同,有一种方法 istream::readfread 非常相似。【参考方案3】:

如您所见,编译器在此类大数据数组上崩溃。

除了读取二进制文件(因为您不想这样做)之外,您还可以做的是与汇编文件链接。它仍然使可执行文件自给自足,并且 GAS 对大文件的容忍度更高。这是我使用 python 生成的一些 asm 文件的示例,它与经典的 gcc 组装得很好:

.section .rodata
.globl FT
.globl FT_end
FT:
.byte  0x46,0x54,0x5f,0x43,0x46,0x0,0x0,0x0,0x0,0x0,0x0,0x3,0x43,0x4f,0x4d,0x50
.byte  0x32,0x30,0x31,0x0,0x3,0x88,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0
.byte  0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0
.byte  0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x28,0xe6,0x47,0x6,0x7,0x8,0x28,0x28
.byte  0x26,0x6,0x2a,0x6,0x6,0x40,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0
FT_end:

该技术允许我在可执行文件中嵌入一个 80 MB 的二进制文件(100 万行代码),因为我没有文件系统来读取该环境中的数据文件 (QEMU)

用我终于可以挖掘出来的python代码进行实际测试:

Python 代码:

floats = [0.12,0.45,0.34,4.567,22.7]

import struct
contents = struct.pack('f'*len(floats), *floats)

outbase = "_extref"
output_file = "data.s"

fw = open(output_file,"w")
fw.write(""".section .rodata
.globl 0
.globl 0_end
0:
""".format(outbase,outbase))
eof = False
current_offset = 0

while not eof:
    to_write = []
    if current_offset==len(contents):
        break
    if current_offset<len(contents):
        fw.write(".byte  ")
        for i in range(0,16):
            if current_offset<len(contents):
                to_write.append(hex(ord(contents[current_offset])))
                current_offset+=1
            else:
                eof = True
                break
        if len(to_write)>0:
            fw.write(",".join(to_write)+"\n")

fw.write(outbase+"_end:\n")
fw.close()

test.cpp:C++ 代码(C++11,我为 asm 部分的指针引用而苦苦挣扎):

#include <iostream>
#include <vector>

#include <strings.h>

extern const float extref;
extern const float extref_end;

int main()

    int size = (&extref_end - &extref);
    std::cout << "nb_elements: " << size << std::endl;
    std::vector<float> v(size);
    memcpy(&v[0],&extref,sizeof(float)*size);

    for (auto it : v)
    
      std::cout << it << std::endl;
    
    return 0;


Python 代码生成一个data.s 文件。创建一个可执行文件:

g++ -std=c++11  test.cpp data.s

运行:

nb_elements: 5
0.12
0.45
0.34
4.567
22.7

此方法的主要优点是您可以使用所需的格式定义任意数量的符号。

【讨论】:

以上是关于在 C++ 中初始化非常大的向量的主要内容,如果未能解决你的问题,请参考以下文章

运行时在构造函数中初始化向量类成员——C++

如何在 C++ 中使用构造函数初始化二维向量?

如何在 C++ 中声明和初始化 2d int 向量?

如何初始化 2D 向量 C++ [重复]

我们如何在 C++ 中初始化所有值为 0 的向量 [重复]

如何在 MS 编译器中使用数组初始化 C++ 向量/集?