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


76

例如,如果一个32位整数溢出,而不是升级intlong,我们可以使用某种40位类型,如果我们只需要2 40以内的范围,那么我们为每一个保存24(64-40)位整数?

如果是这样,怎么办?

我必须面对数十亿美元,而空间是一个更大的约束。


5
与CPU周期相比,内存也很便宜,可以节省宝贵的字节
Ed Heal 2014年

9
@ user1810087,Aniket ...你怎么知道这是不必要的?还是它消耗的字节多于节省的字节?您知道要求和限制吗?也许他处理了TB的数据,这些“少量字节”累加起来了?
2014年

24
@Aniket:我看到了一些用例,尤其是在处理大型数据集时。我目前正在使用1024 ^ 3立方体中的体积模拟。我们已经实现了自定义的36位数据类型,因为这会影响我们的应用程序是否可以与8GB RAM一起使用。示例:1024 ^ 3多维数据集,其中64位= 8192MB,36位= 4608位。在这种情况下,更多的代码实际上并不重要。
BDL 2014年

5
有些处理器以硬件40位整数实现(例如,某些Texas Instruments处理器)。如果您在这些处理器之一上运行,请说是!但是,如果您使用的x86之类的硬件只有32位或64位整数,则成本可能超过使用40位整数的收益。
Trevor Boyd Smith

24
@All:让user1660982决定他/她是否真的想要呢?这里没有人知道数据量或速度是否重要。
deviantfan 2014年

Answers:


82

对,但是...

当然是可能的,但是通常是荒谬的(对于任何不使用数十亿个数字的程序):

#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用完(我没有测试),它看起来可能会非常不同。


1
我在想同样的事情,但是具有超过32位的位域成员是gcc扩展,而不是C标准的一部分(请尝试使用编译代码-Wpedantic)。
bitmask 2014年

2
有趣的...叮当声在这里很好(即使使用-Wpedantic)。和我的GCC一样。使用C ++ 11可以放松对32位的限制吗?
戴蒙2014年

2
虽然这个答案没有错,但它并不能真正回答问题。
user694733 2014年

9
同样,将包含位域的结构填充到基于位域分配单元的结构对齐方式。因此,如果此方法有效,则该结构无论如何都会被填充为8个字节,并且您将不会节省任何空间。
克里斯·多德

3
您可以在大多数编译器中强制执行字节打包(这是一种编译方法,因编译器而异),这会适当减少结构数组。
2014年

54

您可以像这样有效地将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 )
}

13
占了填充实际节省内存的代码段!+1
Ben Voigt

1
优点:实际上节省了空间。缺点:由于编制索引,此代码可能非常慢。
SamB 2014年

2
可能值得signed char hi[4];显式使用。普通char可符号或无符号。
乔纳森·莱夫勒

4
最好使用uint_least32_tint_least8_t在此处,而不是unsigned intcharunsigned int仅要求至少为16位。char将始终至少为8位,因此问题不多。另外,对于hi部分值,我将使用乘法而不是移位。定义明确,如果合适,编译器可以替代位移。除此之外,好主意!
皮特·贝克尔

11
@SamB:目前尚不清楚这会“非常”缓慢。事实是(假设编译器已设置为积极优化(包括内联),因为它应该涉及涉及“数十亿”运算的任何事物!)所有索引都归结为寄存器上的CPU内部运算,这可以完成极少的周期(即快速):通常比从内存中检索缓存行要快得多。由于总的来说,我们现在所访问的内存比以前少了35%(由于节省了空间),因此最终可以取得净赢。(显然,这取决于很多情况-建议进行测量:))
psmears 2015年

25

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

据推测,您已经将许多这些数字存储在某个地方(在RAM中,在磁盘上,通过网络发送等),然后将它们一个接一个地进行处理。

一种方法是使用VLE它们进行编码。来自Google的protobuf文档(CreativeCommons许可)

Varints是一种使用一个或多个字节序列化整数的方法。较小的数字占用较少的字节数。

除了最后一个字节外,varint中的每个字节都设置了最高有效位(msb)–这表明还会有其他字节。每个字节的低7位用于以7位为一组存储数字的二进制补码表示,最低有效组在前。

因此,例如,这里是数字1 –它是一个字节,因此未设置msb:

0000 0001

这是300 –这有点复杂:

1010 1100 0000 0010

您如何确定这是300?首先,从每个字节中删除msb,因为这是在告诉我们是否已到达数字的末尾(如您所见,它设置在第一个字节中,因为varint中有多个字节)

优点

  • 如果您有很多小数字,则每个整数平均可能使用少于40个字节。可能少得多。
  • 您将来可以存储更大的数字(超过40位),而不必为小数字付费

缺点

  • 您需要为数字的每7个有效位支付额外的费用。这意味着具有40个有效位的数字将需要6个字节。如果您的大多数数字都有40个有效位,那么最好使用位域方法。
  • 您将无法轻松跳转到给定索引的数字(您必须至少部分解析数组中的所有先前元素才能访问当前元素。
  • 在对数字进行任何有用的处理之前,您将需要某种形式的解码(尽管对于其他方法也是如此,例如位字段)

您可以将最小单位更改为16或32位,因此,如果大多数值大于1个字节但适合15或31位,则可以节省大量内存
phuclv 2014年

3
如果OP试图存储的数字是均匀分布的,那么大数字比小数字要多得多,而可变长度编码将适得其反。
罗素·波罗戈夫

21

(编辑:首先-您想要的是可能的,并且在某些情况下是有道理的;当我尝试为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


与@Andreas达成协议后,使用可预测的代码生成就很简单,这与使用位字段或依赖于编译器特定的打包的答案不同。constexpr-ified C ++ 17实现。
ildjarn '16

16

您可以使用位域结构,但这不会节省任何内存:

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

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


@barakmanos:您确定您的新版本更好吗?
Ben Voigt 2014年

@BenVoigt:在VC2013上可以。我不确定100%是否根据语言标准这样做,还是取决于编译器。如果是后者,那么a#pragma pack应该做“其余的工作”。顺便说一下,这里还有其他问题,例如CHAR_BIT理论上可以大于8,或者sizeof(short)理论上可以为1(例如,如果CHAR_BIT为16)。我更喜欢使答案简单易读,而不是指出所有这些极端情况。
barak manos 2014年

1
@MarcGlisse乘以64表示8,因为sizeof计算字节数。
user253751

1
@Inverse:谢谢,但是您对第一部分的编辑使第二部分的开头声明毫无意义。此外(甚至更糟),这是错误的-sizeof(my_struct)每个编译器(或任何编译器)都不是5个字节。而且在任何情况下,您都无法实例化一个将反映每个条目5个字节的结构数组。在提交更改之前,请先验证您所做的更改(尤其是其他用户的回答)。
barak manos 2014年

@immibis不,我的意思是64,但是该评论在编辑之前发布(请查看历史记录,如果您想了解它的含义)。
Marc Glisse 2015年

9

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

除非您想节省大量RAM,否则可能是不必要的麻烦-这更有意义。(RAM节省是long在RAM中存储的数百万个值中节省的位的总和)

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

要使用这些值,然后将这些位移回along即可使用该值。

然后,您的RAM和文件存储的值将是40位(5个字节),但是,如果您打算使用astruct来保存5个字节,则必须考虑数据对齐。让我知道您是否需要详细说明这种移位和数据对齐的含义。

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


6

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

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

这样的结构将占用16个字节,并且如果对齐16个字节,将完全适合单个高速缓存行。虽然确定使用结构的哪个部分可能比如果该结构包含四个元素而不是三个元素要贵,但是访问一个缓存行可能比访问两个便宜得多。如果性能很重要,则应使用某些基准,因为某些计算机可能便宜地执行divmod-3操作,并且每次高速缓存行获取的成本较高,而其他计算机可能具有更便宜的内存访问权限和更昂贵的divmod-3。


注意,divmod-3实际上可能是通过乘法来完成的。
SamB 2014年

@SamB:通常确实最好通过某种乘法来完成,但是在实现之间可能有所不同。在像Cortex-M0之类的东西上,任意32位数字的divmod3会有些昂贵,并且对数字的32位部分和40位部分进行完全单独的提取不会有问题。
2014年

6

我假设

  1. 这是C,并且
  2. 您需要一个由40位数字组成的大型数组,并且
  3. 您使用的是低位优先的计算机,并且
  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);
}

4
请注意,代码包含未定义的行为,因为unsigned char不能保证正确对齐__int64。在某些平台上,如X86-64,它不会可能会对未优化编译太大的影响(期望的性能损失),但别人是有问题的-如ARM。在优化的版本上,所有赌注都关闭了,因为允许编译器例如使用产生代码movaps
Maciej Piechotka 2014年

1
可能是所有方法中最简单的解决方案!
anatolyg 2014年

当然,在所有类型转换中,这在C语言中看起来都很难看,但是生成的机器代码将简单快速。get的shift版本很可能会更快,因为它不会分支。通过从数字前的3个字节读取可以进一步优化它,从而节省了左移。
aaaaaaaaaaaa 2014年

1
您可以通过这种方式将其留给编译器来进行有效的符号扩展。但是,应仔细测试,因为未对齐的访问可能会非常昂贵。像在其他一些解决方案中一样,分别存储第5个字节可能会更好
phuclv 2015年

1
您可以使用它memcpy来轻松地表达未对齐的加载/存储,而不会像指针指针那样出现任何严格混叠的冲突。针对x86(或其他具有有效未对齐负载的平台)的现代编译器将仅使用未对齐负载或存储。例如,这里(godbolt.org/g/3BFhWf)是Damon的40位整数C ++类的修改版本,该类使用achar value[5]并将其与x86-64的gcc编译为相同的asm。(如果您使用的是过度读取的版本,而不是进行单独的加载,但这也相当不错)
Peter Cordes

5

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

我认为您假设符号扩展为2的补码。它认为它会以符号/幅度中断,但可能以1的补码起作用。无论如何,对于2的补码,要求编译器为您将最后一个字节符号扩展为64位,然后对低半部分进行OR运算可能会更轻松,更高效。(然后x86编译器可以使用movsx字节加载,移位,然后在低32位中使用OR。大多数其他体系结构也具有符号扩展的窄负载)。做你想做的。
彼得·科德斯

@PeterCordes:谢谢,那里有一个未提及的二进制补码形式的假设,是的。不知道为什么我要依靠它。令人费解。
干杯和健康。-Alf

我不会为了使效率可移植到没有人会使用它的平台而牺牲效率。但是,如果可能的话,可以static_assert用来检查您所依赖的语义。
彼得·科德斯

5

如果你要处理数十亿美元的整数,我想尝试时封装阵列的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:显然,这是概念验证代码,而不是可用于生产的代码。要在实际项目中使用它,您必须添加(除其他外):

  • 错误处理(malloc可能会失败!)
  • 复制构造函数(例如,通过复制数据,添加引用计数或将复制构造函数设为私有)
  • 移动构造函数
  • const重载
  • 兼容STL的迭代器
  • 边界检查索引(在调试版本中)
  • 范围检查值(在调试版本中)
  • 断言隐含假设(小端)
  • 正因为如此,Item有参考语义,不值语义,这是不寻常的operator[]; 您可能可以通过一些巧妙的C ++类型转换技巧来解决此问题

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


@anatolyg:我已经尝试在注释2中总结您的观点。欢迎您添加到该列表中;-)
Niki 2015年

3

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

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

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

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

由于您的问题不是很具体,我在这里没有添加更多详细信息。您知道要存储多少个整数吗?您在编译时知道吗?程序启动时您知道吗?整数应如何组织?像数组一样?像地图一样?在尝试将整数压缩为更少的存储空间之前,您应该了解所有这些信息。


每个“块”可以将40 * 64 = 2560bit减少为1cm(40,64)= 320bit。5 64bit-ints
deviantfan

3
std::vector<>绝对不是走的路:它具有至少三个指针的足迹,即96或192位,具体取决于体系结构。这比a的64位差得多long long
cmaster-恢复莫妮卡2014年

3
依靠。一个100000000整数的std :: vector可以。如果我们要像其他答案一样设计小块,则std :: vector将浪费空间。
汉斯·克鲁德(HansKlünder)2014年

3

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

我们通常将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个字节。大多数处理器将强制对齐,您将不会保存单个字节。
  • 请使用同质的数据结构,例如char数组或64位整数数组。这是迄今为止更容易得到正确对齐。 (而且您还保留对打包的控制权,这意味着您可以小心地将数组划分为多个线程以进行计算)
  • 如果必须同时支持两种平台,请为32位和64位处理器设计单独的解决方案。因为您正在做的事情非常低级且受支持不高,所以您需要针对每种算法自定义其内存架构。
  • 请记住,40位数字的乘法与将40位数字的64位扩展乘以40扩展到40位不同。就像处理x87 FPU一样,您必须记住在位大小之间编组数据会改变结果。

如果您的数字是连续的(例如,struct { char val[5]; };使用memcpy),则多个加载或存储将位于同一高速缓存行中。这是便宜的(如果您之前没有遇到指令或L1D吞吐量方面的瓶颈),并且不会造成额外的高速缓存未命中,但是会打败自动矢量化,因此您甚至可能没有足够的内存来进行顺序访问。(通常,您希望它在支持不对齐负载的目标上编译为32位+ 8位负载。现代x86对缓存行分割的惩罚很低,尽管当字负载分割成4k页时,罚款较高)。
彼得·科德斯

涉及分支的打包/解包策略是可行的,但几乎不值得一提,除非您手动获得超低级的uintptr_t和对齐检查/宽负载(例如您可能在asm中考虑)。还是您在谈论在此之上uint64_t []并使用anif来确定是否仅需要一个负载?与仅使用移位将uint64_t与uint32_tand进行拆分/合并uint8_t以及memcpy或使用结构进行分组以进行对齐相比,这听起来像是个坏主意。
彼得·科德斯

根据ISO C ++ 11,您可以使用零宽度的位域来分隔“存储位置”。我不确定标准是否意味着一个数组struct __attribute__((packed)) { unsigned long long v:40; };是否真的会是一个巨大的内存位置。但即使结构边界不是内存位置边界,您也可以使用int end:0来保证(模编译器错误,以及!stackoverflow.com/questions/47008183/...
彼得·科德斯

3

这请求流式传输内存中的无损压缩。如果这是针对大数据应用程序,那么对于似乎需要相当不错的中间件或系统级支持的情况,密集打包技巧最多是战术解决方案。他们需要进行彻底的测试,以确保能够恢复所有不受损害的位。由于对CPU缓存体系结构(例如,缓存行与打包结构)的干扰,对性能的影响非常重要,并且与硬件非常相关。有人提到复杂的网格划分结构:通常会对其进行微调以与特定的缓存体系结构配合使用。

从需求尚不清楚OP是否需要随机访问。考虑到数据的大小,很有可能只需要在相对较小的块上进行局部随机访问,就可以分层组织以便进行检索。甚至硬件也可以在大内存(NUMA)的情况下执行此操作。就像无损电影格式所示,应该有可能以块(“帧”)的形式进行随机访问,而不必将整个数据集加载到热内存中(从压缩的内存后备存储中)。

我知道一个快速数据库系统(KX Systems的kdb就是其中一个,但我知道还有其他数据库)可以通过将后备存储器中的大型数据集进行内存映射来处理超大型数据集。它可以选择透明地实时压缩和扩展数据。


2

如果您真正想要的是一个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位);访问项目的代码稍微复杂一些,但是项目的两个组件都在同一缓存行中。


这不会对缓存造成可怕的影响吗?
SamB 2014年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.