如果32位整数溢出,我们可以使用40位结构而不是64位长结构吗?

比如说,如果一个32位整数溢出,而不是将 int升级到 long,那么如果我们只需要240范围内的一个范围,那么我们是否可以使用一些40位类型,以便为每个整数节省24(64-40)位?

如果是这样,怎么做?

我必须处理数十亿和空间是一个更大的约束。

13507 次浏览

您可以使用位字段结构,但它不会为您节省任何内存:

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位变量相比) ,但是您必须将每个变量上的每个操作(特别是算术操作)拆分为多个操作。

因此,内存优化将被“交易”为运行时性能。

是的,但是..。

它当然是 有可能,但它通常是无意义的(对于任何程序,不使用这些数字的 数十亿) :

#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位或存储你的40位值在结构8的每个或其倍数)。
节省三个字节的 无数次是值得的,因为它将需要明显更少的内存页面,从而导致更少的缓存和 TLB 丢失,尤其是所有的页面错误(一个页面错误加权数千万条指令)。

虽然上面的代码片段没有使用剩余的24位(它只是演示了“使用40位”部分) ,但是类似于下面这样的东西对于保存内存的方法来说是非常有用的——假设你确实有其他“有用”的数据可以填补空白:

struct using_gaps
{
uint64_t var           : 40;
uint64_t useful_uint16 : 16;
uint64_t char_or_bool  : 8;
};

结构大小和对齐方式将等于一个64位整数,因此,如果使用10亿个这样的结构数组(即使不使用编译器特定的扩展) ,也不会浪费任何东西。如果您不需要8位值,也可以使用48位和16位值(提供更大的溢出空间)。
或者,您可以牺牲可用性,将8个40位值放入一个结构中(40和64的最小公倍数为320 = 8 * 40)。当然,那么访问结构数组中元素的代码将变得更加复杂(尽管可以实现一个恢复线性数组功能并隐藏结构复杂性的 operator[])。

更新:
编写了一个快速测试套件,只是为了看看 bitfields (以及 bitfield 参考运算符重载)会有什么开销。张贴代码(由于长度)在 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,它可能看起来非常不同(我没有测试这一点)。

是的,你可以这样做,它将为大量的数字节省一些空间

您需要一个包含无符号整数类型的 std: : Vector 的类。

您将需要成员函数来存储和检索整数。例如,如果你想存储每个40位的64个整数,使用一个每个64位的40个整数的向量。然后,您需要一个方法,该方法将一个带索引的整数存储在[0,64]中,并且需要一个方法来检索这样一个整数。

这些方法将执行一些 shift 操作,以及一些二进制 | 和 & 。

我不会在这里添加任何更多的细节,因为你的问题不是很具体。您知道要存储多少个整数吗?您在编译时知道它吗?你知道节目什么时候开始吗?整数应该如何组织?像数组一样?比如地图?在试图将整数压缩到更少的存储空间之前,您应该了解所有这些。

正如评论所暗示的那样,这是一项艰巨的任务。

可能是一个不必要的麻烦 除非你想节省大量的内存-然后它使得更多的意义。(RAM 节省是指存储在 RAM 中的数百万个 long值中节省的位的总和)

我会考虑使用一个5字节/char 的数组(5 * 8位 = 40位)。然后,您需要将位从(溢出 int-因此是 long)值转移到字节数组中来存储它们。

要使用这些值,然后将这些位移回到 long中,您就可以使用这个值了。

然后您的 RAM 和文件存储的值将是40位(5字节) ,但是您必须考虑数据对齐,如果您计划使用 struct保持5字节。如果您需要详细说明这个位移和数据对齐的含义,请告诉我。

类似地,您可以在剩余的24位中使用不希望使用的64位 long藏起来其他值(可能是3个字符)。再次-使用位移来添加和删除24位值。

你可以非常有效地将4 * 40位整数打包成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 )
}

(编辑: 首先——你想要的东西是可能的,在某些情况下是有意义的; 当我尝试为 Netflix 挑战做一些事情时,我不得不做类似的事情,因为我只有1GB 的内存;第二,最好在40位存储器中使用字符数组,以避免任何对齐问题和混乱的结构打包杂注; 第三,这个设计假设你对中间结果的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

您可能需要考虑可变长度编码(VLE)

据推测,您已经在某个地方存储了大量这些数字(在 RAM 中、磁盘上、通过网络发送它们等) ,然后逐个获取它们并进行一些处理。

一种方法是使用 VLE 对它们进行 编码。 来自 Google 的原型程序 文件(CreativeCommons 许可证)

变量是使用以下方法序列化整数的方法 一个或多个字节。较小的数字占用较小的字节数。

变量中的每个字节(最后一个字节除外)具有最高有效性 Bit (msb) set-这表示还有更多的字节需要处理。 每个字节的较低7位用于存储两个补码 以7位组成的数字的表示形式,最低有效性 小组第一。

例如,这里是数字1-它是一个单字节,所以 msb 未设定:

0000 0001

这里是300-这有点复杂:

1010 1100 0000 0010

你怎么知道这是300? 首先你把 msb 从 每个字节,因为它只是告诉我们是否已经到达 数字的结尾(正如你所看到的,它被设置在第一个字节中 在 varint 中多于一个字节)

优点

  • 如果有很多小数,那么平均来说,每个整数使用的字节数可能少于40个。可能更少。
  • 您可以在未来存储更大的数字(超过40位) ,而不必为较小的数字付出代价

缺点

  • 你需要为你的数字的每7个重要位额外支付一位。这意味着一个有40个有效位的数字将需要6个字节。如果您的大多数数字有40个有效位,那么您最好使用位字段方法。
  • 如果给定一个数字的索引,您将失去轻松跳转到该数字的能力(为了访问当前数组,您必须至少部分解析数组中以前的所有元素。
  • 在使用这些数字做任何有用的事情之前,您将需要某种形式的解码(尽管对于其他方法也是如此,比如位字段)

如果你必须处理数十亿个整数,我会尝试封装 数组的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);
}
};

注意: 从40位到64位的 memcpy转换基本上是未定义行为的,因为它假设了 litte-endianness。不过,它应该可以在 x86平台上工作。

注2: 显然,这是概念验证代码,而不是生产就绪的代码。要在实际项目中使用它,您必须添加(除其他外) :

  • 错误处理(malloc 可能会失败!)
  • 复制建构子(例如复制资料、加入参考点算或将复制建构子私有化)
  • 移动构造函数移动构造函数
  • Const 过载
  • 兼容 STL 的迭代器
  • 对索引的边界检查(在调试构建中)
  • 范围检查值(在调试构建中)
  • 隐式假设的断言(little-endianness)
  • 实际上,Item具有引用语义,而不是值语义,这对于 operator[]来说是不寻常的; 您可以使用一些聪明的 C + + 类型转换技巧来解决这个问题

对于 C + + 程序员来说,所有这些都应该是简单明了的,但是它们会使示例代码变得更长,而且不会变得更清晰,所以我决定省略它们。

我想是的

  1. 这是 C
  2. 您需要一个由40位数字组成的单个大数组,并且
  3. 你在一台 Little-Endian 机器上
  4. 你的机器足够聪明,可以处理校准
  5. 您已经将大小定义为所需的40位数字的数目

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);
}

如果你真正想要的是一个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字节结构,其中包含一个包含6个 uint32 _ t 的数组、一个包含6个 uint8 _ t 的数组和两个未使用的字节(每个数字412/3 rd) ; 访问一个项的代码稍微复杂一些,但是该项的两个组件都在同一个缓存行中。

另一个可能有帮助的变体是使用一种结构:

typedef struct TRIPLE_40 {
uint32_t low[3];
uint8_t hi[3];
uint8_t padding;
};

这样的结构将占用16字节,如果16字节对齐,将完全适合单个缓存线路。虽然确定结构中哪些部分的使用可能比结构中包含四个元素而不是三个元素的成本更高,但访问一个缓存线路可能比访问两个要便宜得多。如果性能很重要,那么应该使用一些基准测试,因为一些机器可能执行 divmod-3操作的成本较低,每次缓存线路读取的成本较高,而另一些机器可能具有更便宜的内存访问和更昂贵的 divmod-3。

这就需要内存中的流式无损数据压缩。如果这是针对大数据应用程序的,密集打包技巧充其量只是需要相当不错的中间件或系统级支持的战术解决方案。他们需要彻底的测试,以确保能够恢复所有的位完好无损。而且由于对 CPU 缓存架构的干扰(例如缓存线路与封装结构) ,性能影响非常重要,并且非常依赖于硬件。有人提到了复杂的网状结构: 这些结构通常经过了微调,以便与特定的缓存架构协同工作。

从需求中并不清楚 OP 是否需要随机访问。考虑到数据的大小,很可能只需要对相对较小的数据块进行本地随机访问,并按层次进行检索。即使是硬件在大内存大小(NUMA)时也会这样做。正如无损电影格式所显示的那样,应该可以在不必将整个数据集加载到热内存(从压缩的内存备份存储)的情况下获得块(“帧”)的随机访问。

我知道有一个快速数据库系统(KX Systems 的 kdb 就是其中之一,但我知道还有其他系统)可以通过从后台存储无缝内存映射大型数据集来处理极大的数据集。它可以选择透明地压缩和扩展动态数据。

对于存储数十亿个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;
}

这里有很多关于实现的答案,所以我想谈谈架构。

我们通常将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位寄存器中才能使用它(这甚至可能涉及到一个难以预测的分支)。

这完全有可能是物有所值的

在某些情况下,这些惩罚是可以接受的。如果您有大量的内存驻留数据,而且这些数据都有很好的索引,那么您可能会发现节省的内存值得付出性能代价。如果您对每个值进行大量的计算,您可能会发现成本是最小的。如果是这样,请随意实现上述解决方案之一。然而,这里有一些建议。

  • 不要使用位字段,除非你已经准备好支付它们的成本。例如,如果您有一个位字段数组,并希望将其划分为多个线程进行处理,那么您将陷入困境。根据 C + + 11的规则,位字段都形成一个内存位置,因此一次只能由一个线程访问 (这是因为打包位字段的方法是实现定义的,所以 C + + 11不能帮助您以非实现定义的方式分发它们)
  • 不要使用包含32位整数和 char 的结构来构成40个字节。大多数处理器将强制对齐,而您不会保存一个字节。
  • 一定要使用同构的数据结构,比如字符数组或64位整数数组。这是 很远更容易得到正确的对齐。(并且您还保留了对打包的控制,这意味着如果您小心的话,您可以将一个数组划分到几个线程中进行计算)
  • 如果必须同时支持32位和64位处理器,那么请为它们设计单独的解决方案。因为您所做的事情级别非常低,支持也非常差,所以您需要根据其内存架构自定义每个算法。
  • 请记住,40位数字的乘法不同于64位40位数字的乘法。就像处理 x87FPU 一样,您必须记住在位大小之间编组数据会改变结果。