例如,如果一个32位整数溢出,而不是升级int
到long
,我们可以使用某种40位类型,如果我们只需要2 40以内的范围,那么我们为每一个保存24(64-40)位整数?
如果是这样,怎么办?
我必须面对数十亿美元,而空间是一个更大的约束。
例如,如果一个32位整数溢出,而不是升级int
到long
,我们可以使用某种40位类型,如果我们只需要2 40以内的范围,那么我们为每一个保存24(64-40)位整数?
如果是这样,怎么办?
我必须面对数十亿美元,而空间是一个更大的约束。
Answers:
当然是可能的,但是通常是荒谬的(对于任何不使用数十亿个数字的程序):
#include <stdint.h> // don't want to rely on something like long long
struct bad_idea
{
uint64_t var : 40;
};
在这里,var
确实要有40位的宽度,但要付出很大的代价生成的效率低(事实证明,“很多”是非常错误的-测量的开销仅为1-2%,请参见下面的时序),并且通常无济于事。除非您需要将另一个24位值(或8位和16位值)打包成相同的结构,否则对齐将丧失您可能获得的任何东西。
无论如何,除非您有数十亿个这样的内存,否则内存消耗的有效差异将不会很明显(但是管理位字段所需的额外代码将非常明显!)。
注意:
在此同时,问题已经更新,以反映确实需要数十亿个数字,因此这可能是可行的,前提是您要采取措施不因结构对齐和填充而损失收益,即通过在剩余的24位中存储其他内容,或通过将8位或以上的40位值存储在结构中)。十亿次
节省三个字节是值得的,因为它将需要显着更少的内存页面,从而导致更少的缓存和TLB丢失,尤其是所有页面错误(单个页面错误权重数千万条指令)。
尽管上面的代码段没有使用剩余的24位(它仅演示了“使用40位”部分),但要使该方法在保留内存的意义上真正有用,则需要类似于以下内容:您确实还有其他“有用的”数据需要填补:
struct using_gaps
{
uint64_t var : 40;
uint64_t useful_uint16 : 16;
uint64_t char_or_bool : 8;
};
结构的大小和对齐方式将等于64位整数,因此,如果您制作了一个十亿个这样的结构的数组(即使不使用编译器专有的扩展),也不会浪费任何资源。如果您不使用8位值,则也可以使用48位和16位值(溢出余量更大)。
或者,您可以牺牲可用性来将8个40位值放入一个结构中(40和64的最小公倍数为320 = 8 * 40)。当然那么你的代码,其访问结构的阵列中的元素将变得多少更复杂(尽管或许可以实现一个operator[]
能恢复线性阵列功能和生皮结构的复杂性)。
更新:
编写了一个快速测试套件,只是为了查看位域的开销(以及操作员对位域引用的重载)。在gcc.godbolt.org上发布的代码(由于长度),我的Win7-64机器的测试输出为:
Running test for array size = 1048576
what alloc seq(w) seq(r) rand(w) rand(r) free
-----------------------------------------------------------
uint32_t 0 2 1 35 35 1
uint64_t 0 3 3 35 35 1
bad40_t 0 5 3 35 35 1
packed40_t 0 7 4 48 49 1
Running test for array size = 16777216
what alloc seq(w) seq(r) rand(w) rand(r) free
-----------------------------------------------------------
uint32_t 0 38 14 560 555 8
uint64_t 0 81 22 565 554 17
bad40_t 0 85 25 565 561 16
packed40_t 0 151 75 765 774 16
Running test for array size = 134217728
what alloc seq(w) seq(r) rand(w) rand(r) free
-----------------------------------------------------------
uint32_t 0 312 100 4480 4441 65
uint64_t 0 648 172 4482 4490 130
bad40_t 0 682 193 4573 4492 130
packed40_t 0 1164 552 6181 6176 130
可以看到的是,位域的额外开销可以忽略不计,但是当以缓存友好的方式线性访问数据时,操作员将位域引用作为方便的事情而进行的重载相当剧烈(大约增加了3倍)。另一方面,对于随机访问它几乎没有关系。
这些时间表明,简单地使用64位整数会更好,因为它们总体上仍比位域更快(尽管会占用更多内存),但是当然,它们没有考虑使用更大数据集的页面错误的代价。一旦物理RAM用完(我没有测试),它看起来可能会非常不同。
-Wpedantic
)。
您可以像这样有效地将4 * 40bits整数打包成160位结构:
struct Val4 {
char hi[4];
unsigned int low[4];
}
long getLong( const Val4 &pack, int ix ) {
int hi= pack.hi[ix]; // preserve sign into 32 bit
return long( (((unsigned long)hi) << 32) + (unsigned long)pack.low[i]);
}
void setLong( Val4 &pack, int ix, long val ) {
pack.low[ix]= (unsigned)val;
pack.hi[ix]= (char)(val>>32);
}
这些可以再次这样使用:
Val4[SIZE] vals;
long getLong( int ix ) {
return getLong( vals[ix>>2], ix&0x3 )
}
void setLong( int ix, long val ) {
setLong( vals[ix>>2], ix&0x3, val )
}
signed char hi[4];
显式使用。普通char
可符号或无符号。
uint_least32_t
和int_least8_t
在此处,而不是unsigned int
和char
。unsigned int
仅要求至少为16位。char
将始终至少为8位,因此问题不多。另外,对于hi
部分值,我将使用乘法而不是移位。定义明确,如果合适,编译器可以替代位移。除此之外,好主意!
据推测,您已经将许多这些数字存储在某个地方(在RAM中,在磁盘上,通过网络发送等),然后将它们一个接一个地进行处理。
一种方法是使用VLE对它们进行编码。来自Google的protobuf文档(CreativeCommons许可)
Varints是一种使用一个或多个字节序列化整数的方法。较小的数字占用较少的字节数。
除了最后一个字节外,varint中的每个字节都设置了最高有效位(msb)–这表明还会有其他字节。每个字节的低7位用于以7位为一组存储数字的二进制补码表示,最低有效组在前。
因此,例如,这里是数字1 –它是一个字节,因此未设置msb:
0000 0001
这是300 –这有点复杂:
1010 1100 0000 0010
您如何确定这是300?首先,从每个字节中删除msb,因为这是在告诉我们是否已到达数字的末尾(如您所见,它设置在第一个字节中,因为varint中有多个字节)
优点
缺点
(编辑:首先-您想要的是可能的,并且在某些情况下是有道理的;当我尝试为Netflix挑战做一些事情并且只有1GB的内存时,我不得不做类似的事情;第二-最好将char数组用于40位存储,以避免任何对齐问题以及避免弄乱struct pack pragma的问题;第三-此设计假定您可以使用64位算术来获得中间结果,但这仅适用于大型您将使用Int40的数组存储;第四:我没有得到所有建议,认为这不是一个好主意,只是仔细阅读人们打包网格数据结构所经历的过程,这看起来就像是孩子们的游戏。
您想要的是一个仅用于将数据存储为40位int的结构,但是将其隐式转换为int64_t以进行算术运算。唯一的技巧是将符号从40位扩展到64位。如果您适合使用无符号整数,则代码甚至可以更简单。这应该能够帮助您入门。
#include <cstdint>
#include <iostream>
// Only intended for storage, automatically promotes to 64-bit for evaluation
struct Int40
{
Int40(int64_t x) { set(static_cast<uint64_t>(x)); } // implicit constructor
operator int64_t() const { return get(); } // implicit conversion to 64-bit
private:
void set(uint64_t x)
{
setb<0>(x); setb<1>(x); setb<2>(x); setb<3>(x); setb<4>(x);
};
int64_t get() const
{
return static_cast<int64_t>(getb<0>() | getb<1>() | getb<2>() | getb<3>() | getb<4>() | signx());
};
uint64_t signx() const
{
return (data[4] >> 7) * (uint64_t(((1 << 25) - 1)) << 39);
};
template <int idx> uint64_t getb() const
{
return static_cast<uint64_t>(data[idx]) << (8 * idx);
}
template <int idx> void setb(uint64_t x)
{
data[idx] = (x >> (8 * idx)) & 0xFF;
}
unsigned char data[5];
};
int main()
{
Int40 a = -1;
Int40 b = -2;
Int40 c = 1 << 16;
std::cout << "sizeof(Int40) = " << sizeof(Int40) << std::endl;
std::cout << a << "+" << b << "=" << (a+b) << std::endl;
std::cout << c << "*" << c << "=" << (c*c) << std::endl;
}
这是尝试运行的链接:http : //rextester.com/QWKQU25252
constexpr
-ified C ++ 17实现。
您可以使用位域结构,但这不会节省任何内存:
struct my_struct
{
unsigned long long a : 40;
unsigned long long b : 24;
};
您可以将8个这样的40位变量的任意倍数压缩为一个结构:
struct bits_16_16_8
{
unsigned short x : 16;
unsigned short y : 16;
unsigned short z : 8;
};
struct bits_8_16_16
{
unsigned short x : 8;
unsigned short y : 16;
unsigned short z : 16;
};
struct my_struct
{
struct bits_16_16_8 a1;
struct bits_8_16_16 a2;
struct bits_16_16_8 a3;
struct bits_8_16_16 a4;
struct bits_16_16_8 a5;
struct bits_8_16_16 a6;
struct bits_16_16_8 a7;
struct bits_8_16_16 a8;
};
这将为您节省一些内存(与使用8个“标准” 64位变量相比),但是您必须将每个变量的每个操作(尤其是算术操作)拆分为多个操作。
因此,内存优化将被“换”为运行时性能。
#pragma pack
应该做“其余的工作”。顺便说一下,这里还有其他问题,例如CHAR_BIT
理论上可以大于8,或者sizeof(short)
理论上可以为1(例如,如果CHAR_BIT
为16)。我更喜欢使答案简单易读,而不是指出所有这些极端情况。
sizeof
计算字节数。
sizeof(my_struct)
每个编译器(或任何编译器)都不是5个字节。而且在任何情况下,您都无法实例化一个将反映每个条目5个字节的结构数组。在提交更改之前,请先验证您所做的更改(尤其是其他用户的回答)。
正如评论所暗示的,这是一项艰巨的任务。
除非您想节省大量RAM,否则可能是不必要的麻烦-这更有意义。(RAM节省是long
在RAM中存储的数百万个值中节省的位的总和)
我会考虑使用5个字节/字符的数组(5 * 8位= 40位)。然后,您需要将位从您的(溢出的int-因此是long
)值移到字节数组中以存储它们。
要使用这些值,然后将这些位移回along
即可使用该值。
然后,您的RAM和文件存储的值将是40位(5个字节),但是,如果您打算使用astruct
来保存5个字节,则必须考虑数据对齐。让我知道您是否需要详细说明这种移位和数据对齐的含义。
类似地,您可以使用64位long
,并在不想使用的剩余24位中隐藏其他值(也许3个字符)。再次-使用位移来添加和删除24位值。
另一种可能有用的变体是使用一种结构:
typedef struct TRIPLE_40 {
uint32_t low[3];
uint8_t hi[3];
uint8_t padding;
};
这样的结构将占用16个字节,并且如果对齐16个字节,将完全适合单个高速缓存行。虽然确定使用结构的哪个部分可能比如果该结构包含四个元素而不是三个元素要贵,但是访问一个缓存行可能比访问两个便宜得多。如果性能很重要,则应使用某些基准,因为某些计算机可能便宜地执行divmod-3操作,并且每次高速缓存行获取的成本较高,而其他计算机可能具有更便宜的内存访问权限和更昂贵的divmod-3。
我假设
unsigned char hugearray[5*size+3]; // +3 avoids overfetch of last element
__int64 get_huge(unsigned index)
{
__int64 t;
t = *(__int64 *)(&hugearray[index*5]);
if (t & 0x0000008000000000LL)
t |= 0xffffff0000000000LL;
else
t &= 0x000000ffffffffffLL;
return t;
}
void set_huge(unsigned index, __int64 value)
{
unsigned char *p = &hugearray[index*5];
*(long *)p = value;
p[4] = (value >> 32);
}
两班制可能会更快地处理获取操作。
__int64 get_huge(unsigned index)
{
return (((*(__int64 *)(&hugearray[index*5])) << 24) >> 24);
}
unsigned char
不能保证正确对齐__int64
。在某些平台上,如X86-64,它不会可能会对未优化编译太大的影响(期望的性能损失),但别人是有问题的-如ARM。在优化的版本上,所有赌注都关闭了,因为允许编译器例如使用产生代码movaps
。
memcpy
来轻松地表达未对齐的加载/存储,而不会像指针指针那样出现任何严格混叠的冲突。针对x86(或其他具有有效未对齐负载的平台)的现代编译器将仅使用未对齐负载或存储。例如,这里(godbolt.org/g/3BFhWf)是Damon的40位整数C ++类的修改版本,该类使用achar value[5]
并将其与x86-64的gcc编译为相同的asm。(如果您使用的是过度读取的版本,而不是进行单独的加载,但这也相当不错)
对于存储数十亿个40位带符号整数并假设为8位字节的情况,您可以在结构中打包8个40位带符号整数(在下面的代码中使用字节数组来实现),并且,由于此结构通常是对齐的,因此您可以创建此类打包组的逻辑数组,并为其提供普通的顺序索引:
#include <limits.h> // CHAR_BIT
#include <stdint.h> // int64_t
#include <stdlib.h> // div, div_t, ptrdiff_t
#include <vector> // std::vector
#define STATIC_ASSERT( e ) static_assert( e, #e )
namespace cppx {
using Byte = unsigned char;
using Index = ptrdiff_t;
using Size = Index;
// For non-negative values:
auto roundup_div( const int64_t a, const int64_t b )
-> int64_t
{ return (a + b - 1)/b; }
} // namespace cppx
namespace int40 {
using cppx::Byte;
using cppx::Index;
using cppx::Size;
using cppx::roundup_div;
using std::vector;
STATIC_ASSERT( CHAR_BIT == 8 );
STATIC_ASSERT( sizeof( int64_t ) == 8 );
const int bits_per_value = 40;
const int bytes_per_value = bits_per_value/8;
struct Packed_values
{
enum{ n = sizeof( int64_t ) };
Byte bytes[n*bytes_per_value];
auto value( const int i ) const
-> int64_t
{
int64_t result = 0;
for( int j = bytes_per_value - 1; j >= 0; --j )
{
result = (result << 8) | bytes[i*bytes_per_value + j];
}
const int64_t first_negative = int64_t( 1 ) << (bits_per_value - 1);
if( result >= first_negative )
{
result = (int64_t( -1 ) << bits_per_value) | result;
}
return result;
}
void set_value( const int i, int64_t value )
{
for( int j = 0; j < bytes_per_value; ++j )
{
bytes[i*bytes_per_value + j] = value & 0xFF;
value >>= 8;
}
}
};
STATIC_ASSERT( sizeof( Packed_values ) == bytes_per_value*Packed_values::n );
class Packed_vector
{
private:
Size size_;
vector<Packed_values> data_;
public:
auto size() const -> Size { return size_; }
auto value( const Index i ) const
-> int64_t
{
const auto where = div( i, Packed_values::n );
return data_[where.quot].value( where.rem );
}
void set_value( const Index i, const int64_t value )
{
const auto where = div( i, Packed_values::n );
data_[where.quot].set_value( where.rem, value );
}
Packed_vector( const Size size )
: size_( size )
, data_( roundup_div( size, Packed_values::n ) )
{}
};
} // namespace int40
#include <iostream>
auto main() -> int
{
using namespace std;
cout << "Size of struct is " << sizeof( int40::Packed_values ) << endl;
int40::Packed_vector values( 25 );
for( int i = 0; i < values.size(); ++i )
{
values.set_value( i, i - 10 );
}
for( int i = 0; i < values.size(); ++i )
{
cout << values.value( i ) << " ";
}
cout << endl;
}
movsx
字节加载,移位,然后在低32位中使用OR。大多数其他体系结构也具有符号扩展的窄负载)。做你想做的。
static_assert
用来检查您所依赖的语义。
如果你要处理数十亿美元的整数,我想尝试时封装阵列的40位数字,而不是的单40位数字。这样,您可以测试不同的数组实现(例如,一种在运行中压缩数据的实现,或者可能是一种将使用较少的数据存储到磁盘的实现),而无需更改其余代码。
这是一个示例实现(http://rextester.com/SVITH57679):
class Int64Array
{
char* buffer;
public:
static const int BYTE_PER_ITEM = 5;
Int64Array(size_t s)
{
buffer=(char*)malloc(s*BYTE_PER_ITEM);
}
~Int64Array()
{
free(buffer);
}
class Item
{
char* dataPtr;
public:
Item(char* dataPtr) : dataPtr(dataPtr){}
inline operator int64_t()
{
int64_t value=0;
memcpy(&value, dataPtr, BYTE_PER_ITEM); // Assumes little endian byte order!
return value;
}
inline Item& operator = (int64_t value)
{
memcpy(dataPtr, &value, BYTE_PER_ITEM); // Assumes little endian byte order!
return *this;
}
};
inline Item operator[](size_t index)
{
return Item(buffer+index*BYTE_PER_ITEM);
}
};
注意:memcpy
从40位到64位的-conversion基本上是未定义的行为,因为它假定为litte-endianness。不过,它应该可以在x86平台上运行。
注2:显然,这是概念验证代码,而不是可用于生产的代码。要在实际项目中使用它,您必须添加(除其他外):
Item
有参考语义,不值语义,这是不寻常的operator[]
; 您可能可以通过一些巧妙的C ++类型转换技巧来解决此问题对于C ++程序员来说,所有这些都应该是简单明了的,但是它们会使示例代码更长,而没有变得更加清晰,因此我决定省略它们。
是的,您可以这样做,它将为大量数字节省一些空间
您需要一个包含无符号整数类型的std :: vector的类。
您将需要成员函数来存储和检索整数。例如,如果要存储每个40位的64个整数,请使用每个64位的40个整数的向量。然后,您需要一个方法,该方法在索引[0,64]中存储一个带索引的整数,并需要一个检索此类整数的方法。
这些方法将执行一些移位操作,以及一些二进制| 和&。
由于您的问题不是很具体,我在这里没有添加更多详细信息。您知道要存储多少个整数吗?您在编译时知道吗?程序启动时您知道吗?整数应如何组织?像数组一样?像地图一样?在尝试将整数压缩为更少的存储空间之前,您应该了解所有这些信息。
std::vector<>
绝对不是走的路:它具有至少三个指针的足迹,即96或192位,具体取决于体系结构。这比a的64位要差得多long long
。
这里有很多关于实现的答案,所以我想谈谈架构。
我们通常将32位值扩展为64位值以避免溢出,因为我们的体系结构旨在处理64位值。
大多数体系结构都设计为使用2的幂的整数工作,因为这使硬件大大简化了。诸如高速缓存之类的任务用这种方法要简单得多:如果您坚持使用2的幂,则有大量的除法和模运算可以被位掩码和移位所取代。
作为这有多重要的一个示例,C ++ 11规范基于“内存位置”定义了多线程竞争情况。内存位置在1.7.3中定义:
存储器位置是标量类型的对象,或者是全部具有非零宽度的相邻位域的最大序列。
换句话说,如果使用C ++的位域,则必须仔细进行所有多线程处理。即使您希望跨两个相邻位域的计算可以分布在多个线程中,也必须将它们视为相同的内存位置。对于C ++,这是非常不常见的,因此如果您担心它,可能会导致开发人员感到沮丧。
大多数处理器具有一种内存架构,可一次获取32位或64位内存块。因此,使用40位值将具有数量惊人的额外内存访问,从而极大地影响运行时间。考虑对齐问题:
40-bit word to access: 32-bit accesses 64bit-accesses
word 0: [0,40) 2 1
word 1: [40,80) 2 2
word 2: [80,120) 2 2
word 3: [120,160) 2 2
word 4: [160,200) 2 2
word 5: [200,240) 2 2
word 6: [240,280) 2 2
word 7: [280,320) 2 1
在64位体系结构上,每4个字中就有一个是“正常速度”。其余的将需要提取两倍的数据。如果您遇到很多缓存未命中的情况,则可能会破坏性能。即使遇到高速缓存命中,您也将必须解压缩数据并将其重新打包到64位寄存器中才能使用它(这甚至可能涉及难以预测的分支)。
这完全有可能值得
在某些情况下,可以接受这些处罚。如果您拥有大量索引正确的内存驻留数据,则可能会发现值得节省内存的性能。如果对每个值进行大量计算,则可能会发现成本最低。如果是这样,请随时实施上述解决方案之一。但是,这里有一些建议。
struct { char val[5]; };
使用memcpy),则多个加载或存储将位于同一高速缓存行中。这是便宜的(如果您之前没有遇到指令或L1D吞吐量方面的瓶颈),并且不会造成额外的高速缓存未命中,但是会打败自动矢量化,因此您甚至可能没有足够的内存来进行顺序访问。(通常,您希望它在支持不对齐负载的目标上编译为32位+ 8位负载。现代x86对缓存行分割的惩罚很低,尽管当字负载分割成4k页时,罚款较高)。
uintptr_t
和对齐检查/宽负载(例如您可能在asm中考虑)。还是您在谈论在此之上uint64_t []
并使用anif
来确定是否仅需要一个负载?与仅使用移位将uint64_t与uint32_t
and进行拆分/合并uint8_t
以及memcpy或使用结构进行分组以进行对齐相比,这听起来像是个坏主意。
struct __attribute__((packed)) { unsigned long long v:40; };
是否真的会是一个巨大的内存位置。但即使结构边界不是内存位置边界,您也可以使用int end:0
来保证(模编译器错误,以及!stackoverflow.com/questions/47008183/...)
这请求流式传输内存中的无损压缩。如果这是针对大数据应用程序,那么对于似乎需要相当不错的中间件或系统级支持的情况,密集打包技巧最多是战术解决方案。他们需要进行彻底的测试,以确保能够恢复所有不受损害的位。由于对CPU缓存体系结构(例如,缓存行与打包结构)的干扰,对性能的影响非常重要,并且与硬件非常相关。有人提到复杂的网格划分结构:通常会对其进行微调以与特定的缓存体系结构配合使用。
从需求尚不清楚OP是否需要随机访问。考虑到数据的大小,很有可能只需要在相对较小的块上进行局部随机访问,就可以分层组织以便进行检索。甚至硬件也可以在大内存(NUMA)的情况下执行此操作。就像无损电影格式所示,应该有可能以块(“帧”)的形式进行随机访问,而不必将整个数据集加载到热内存中(从压缩的内存后备存储中)。
我知道一个快速数据库系统(KX Systems的kdb就是其中一个,但我知道还有其他数据库)可以通过将后备存储器中的大型数据集进行内存映射来处理超大型数据集。它可以选择透明地实时压缩和扩展数据。
如果您真正想要的是一个40位整数数组(显然您没有),那么我将合并一个32位数组和一个8位整数数组。
要在索引i处读取值x:
uint64_t x = (((uint64_t) array8 [i]) << 32) + array32 [i];
要将值x写入索引i:
array8 [i] = x >> 32; array32 [i] = x;
显然,使用内联函数可以很好地将其封装到类中,以实现最大速度。
在一种情况下,这是次优的,也就是说,当您真正随机访问许多项目时,对int数组的每次访问都将是一个高速缓存未命中-在这里,您每次都会遇到两个高速缓存未命中。为避免这种情况,请定义一个32字节的结构,其中包含一个包含六个uint32_t的数组,一个包含六个uint8_t的数组以及两个未使用的字节(每个数字41 2/3位);访问项目的代码稍微复杂一些,但是项目的两个组件都在同一缓存行中。