【问题标题】:If a 32-bit integer overflows, can we use a 40-bit structure instead of a 64-bit long one?如果 32 位整数溢出,我们可以使用 40 位结构而不是 64 位长的结构吗?
【发布时间】:2015-02-26 14:46:34
【问题描述】:

假设一个 32 位整数溢出,如果我们只需要 240 sup>,这样我们就可以为每个整数节省 24 (64-40) 位?

如果有,怎么做?

我必须处理数十亿,空间是一个更大的限制。

【问题讨论】:

  • 与 CPU 周期相比,为了节省宝贵的字节,内存也非常便宜
  • @user1810087, Aniket... 你怎么知道这是不必要的?或者它消耗的字节多于它的保存?你知道要求和限制吗?也许他处理 TB 的数据,那些“几个字节”加起来?
  • @Aniket:我看到了一些用例,尤其是在处理大型数据集时。我目前正在 1024^3 立方体中进行体积模拟。我们已经实现了一个自定义的 36 位数据类型,因为这会影响我们的应用程序是否可以使用 8GB RAM。示例:1024^3 立方体,64 位 = 8192MB,36 位 = 4608 位。在这种情况下,更多的代码真的没关系。
  • 有些处理器在硬件中实现 40 位整数(例如:一些德州仪器处理器)。如果您在其中一个处理器上运行,我会说是的,请继续!但是,如果您使用的是 x86 等只有 32 位或 64 位整数的硬件,则成本可能会超过使用 40 位整数的好处。
  • @All:让user1660982决定他/她是否真的想要它怎么样?这里没有人知道数据量或速度是否重要。

标签: c++ c memory-management integer-overflow


【解决方案1】:

是的,但是……

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

#include <stdint.h> // don't want to rely on something like long long
struct bad_idea
{
    uint64_t var : 40;
};

这里,var 确实有 40 位的宽度,但会产生 much 效率较低的代码(事实证明,“much”是非常错误的——测量的开销是仅 1-2%,请参见下面的时间安排),通常无济于事。除非您需要将另一个 24 位值(或 8 位和 16 位值)打包到同一结构中,否则对齐将失去您可能获得的任何东西。

无论如何,除非您有数十亿个这样的内存,否则内存消耗的有效差异不会很明显(但管理位字段所需的额外代码会很明显!)。

注意:
同时,该问题已更新,以反映确实需要 十亿 个数字,因此这可能是可行的做法,假设您采取措施不因结构对齐而失去收益和填充,即通过在剩余的 24 位中存储其他内容或将 40 位值存储在每个 8 位或其倍数的结构中)。
节省三个字节十亿次是值得的,因为它将需要明显更少的内存页面,从而导致更少的缓存和 TLB 未命中,尤其是页面错误(单个页面错误加权数千万条指令)。

虽然上面的 sn-p 没有使用剩余的 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[] 来恢复线性数组功能并隐藏结构复杂性)。 p>

更新:
编写了一个快速测试套件,只是为了看看位域(以及使用位域引用重载的运算符)会有什么开销。在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 用完(我没有测试过),它看起来可能会有很大不同。

【讨论】:

  • 我也在想同样的事情,但是超过 32 位的位域成员是 gcc 扩展,而不是 C 标准的一部分(尝试使用 -Wpedantic 编译您的代码)。
  • 有趣... clang 在这里感觉很好(即使使用 -Wpedantic)。我的 GCC 也是如此。 C++11 是否可以放宽对 32 位的限制?
  • 虽然这个答案没有错,但它并没有真正回答问题。
  • 另外,包含位域的结构被填充到结构对齐,这是基于位域分配单元。因此,如果这可行,则结构无论如何都会被填充到 8 个字节,并且您不会节省任何空间。
  • 您可以在大多数编译器中强制字节打包(这是一个编译器因编译器而异的编译指示),这使得结构数组适当地减少。
【解决方案2】:

您可以非常有效地将 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 )
}

【讨论】:

  • 一个代码 sn-p 实际节省内存填充被考虑! +1
  • Pro:这实际上节省了空间。缺点:由于索引,这段代码可能非常慢。
  • 明确地使用signed char hi[4]; 可能是值得的;普通的char 可以签名或未签名。
  • 这里最好使用uint_least32_tint_least8_t,而不是unsigned intcharunsigned int 只需至少为 16 位。 char 将始终至少为 8 位,因此那里没有那么多问题。另外,对于值的hi 部分,我会使用乘法而不是位移;这是很好的定义,如果合适的话,编译器可以替换位移位。除此之外,好主意!
  • @SamB:目前还不清楚这会“非常”慢。问题是(假设编译器设置为积极优化 - 包括内联 - 因为它应该适用于涉及“数十亿”操作的任何事情!)所有索引都归结为寄存器上的 CPU 内部操作,这可以完成在很少的周期内(即快速):通常比从内存中检索高速缓存行快得多。因为总的来说,我们现在访问的内存比以前少了 35%(由于节省了空间),我们最终可以获得净赢。 (显然这取决于很多 - 建议测量:))
【解决方案3】:

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

大概你已经在某个地方存储了很多这些数字(在 RAM 中、磁盘上、通过网络发送它们等),然后将它们一个一个地取出并进行一些处理。

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

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

varint 中的每个字节,除了最后一个字节,都有最重要的 bit (msb) set——这表明还有更多字节要到来。 每个字节的低 7 位用于存储二进制补码 以 7 位为一组表示数字,最低有效 先分组。

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

0000 0001

这里是 300 - 这有点复杂:

1010 1100 0000 0010

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

优点

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

缺点

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

【讨论】:

  • 您可以将最小单位更改为 16 或 32 位,如果大多数值超过 1 个字节但适合 15 或 31 位,则可以节省大量内存
  • 如果 OP 试图存储的数是均匀分布的,那么大数比小数多,变长编码会适得其反。
【解决方案4】:

(编辑:首先 - 你想要什么是可能的,并且在某些情况下是有意义的;当我尝试为 Netflix 挑战做一些事情并且只有 1GB 内存时,我不得不做类似的事情;第二 - 它最好将 char 数组用于 40 位存储,以避免任何对齐问题以及需要弄乱 struct 打包编译指示;第三 - 此设计假设您可以使用 64 位算术来获得中间结果,它是仅适用于您将使用 Int40 的大型数组存储;第四:我没有得到所有关于这是一个坏主意的建议,只需阅读人们通过什么来打包网格数据结构,相比之下这看起来像是孩子们的游戏) .

您想要的是一个仅用于将数据存储为 40 位整数但隐式转换为 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,这对于可预测的代码生成很简单,与使用位字段或依赖编译器特定打包的答案不同。 Hereconstexpr-ified C++17 实现。
【解决方案5】:

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

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:你确定你的新版本会更好吗?
  • @BenVoigt:在 VC2013 上确实如此。我不能 100% 确定的是,它是根据语言标准执行的,还是取决于编译器。如果是后者,那么#pragma pack 应该完成“剩下的工作”。对了,这里还有其他问题,比如CHAR_BIT理论上可以大于8,或者sizeof(short)理论上可以是1(比如CHAR_BIT如果是16)。我更喜欢让答案简单易读,而不是指出所有这些极端情况。
  • @MarcGlisse,64 表示 8,因为 sizeof 计算字节数。
  • @Inverse:谢谢,但是您对第一部分的编辑使第二部分的开场白毫无意义。此外(甚至更糟),这是错误 - sizeof(my_struct) 在每个编译器(或可能在任何编译器)上不是 5 个字节。而且在任何情况下,您都不能实例化一个包含每个条目 5 个字节的结构的数组。请在提交之前验证您的更改(尤其是其他用户的答案)。
  • @immibis 不,我的意思是 64,但该评论是在编辑之前发布的(如果您想了解它的内容,请查看历史记录)。
【解决方案6】:

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

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

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

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

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

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

【讨论】:

    【解决方案7】:

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

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

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

    【讨论】:

    • 请注意,divmod-3 实际上可能通过乘法来完成。
    • @SamB:通常通过某种乘法来做到最好,但这可能因实现而异。在 Cortex-M0 之类的东西上,任意 32 位数字的 divmod3 会有点贵,并且对数字的 32 位部分和 40 位部分进行完全独立的提取是没有问题的。跨度>
    【解决方案8】:

    如果您必须处理数十亿个整数,我会尝试封装 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 位的转换基本上是未定义的行为,因为它假定小端序。不过,它应该可以在 x86 平台上运行。

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

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

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

    【讨论】:

    • @anatolyg:我已尝试在注释 2 中总结您的观点。欢迎您添加到该列表中;-)
    【解决方案9】:

    我假设

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

    【讨论】:

    • 请注意,代码包含未定义的行为,因为 unsigned char 不能保证与 __int64 正确对齐。在某些平台上,例如 x86-64,它可能不会对未优化的构建产生太大影响(预计性能会受到影响),但在其他平台上却是有问题的 - 例如 ARM。在优化的构建上,所有的赌注都被取消了,因为编译器可以使用movaps生成代码。
    • 可能是最简单的解决方案!
    • 当然,这在 C 语言中看起来有点丑陋,但结果机器代码会简单而快速。您的 get 转换版本很可能更快,因为它没有分支。可以通过读取数字前的 3 个字节来进一步优化,从而节省左移。
    • 您可以将其留给编译器使用this way 进行有效的符号扩展。然而,这应该仔细测试,因为未对齐的访问可能非常昂贵。像其他解决方案一样单独存储第 5 个字节可能会更好
    • 您可以使用memcpy 来便携地表达未对齐的加载/存储,也不会出现像指针转换这样的严格混叠违规行为。针对 x86(或其他具有高效未对齐加载的平台)的现代编译器将简单地使用未对齐的加载或存储。例如,这里是 (godbolt.org/g/3BFhWf) Damon 的 40 位整数 C++ 类的破解版,它使用 char value[5] 并使用 gcc for x86-64 编译成与此相同的 asm。 (如果你使用过度读取的版本而不是单独加载,但这也相当不错)
    【解决方案10】:

    对于存储数十亿个 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:谢谢,这里有一个未提及的二进制补码假设,是的。不知道为什么我依赖它。令人费解。
    • 我不会为了让它移植到没人会使用它的平台上而牺牲效率。但如果可能的话,使用static_assert 来检查你所依赖的语义。
    【解决方案11】:

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

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

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

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

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

    【讨论】:

    • 40*64=2560bit 可以减少到每个“块”的 lcm(40,64)=320bit,即。 5 个 64 位整数
    • std::vector&lt;&gt; 绝对不是要走的路:它至少有三个指针的足迹,即。 e. 96 位或 192 位,具体取决于架构。这比 long long 的 64 位差很多
    • 视情况而定。一个用于 100000000 个整数的 std::vector 就可以了。如果我们要像另一个答案那样设计小块,std::vector 将浪费空间。
    【解决方案12】:

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

    我们通常将 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 位数字的乘法不同于 40 位数字的 64 位扩展的乘法,并减少到 40 位。就像处理 x87 FPU 时一样,您必须记住,在位大小之间编组数据会改变您的结果。

    【讨论】:

    • 如果您的数字是连续的(例如 struct { char val[5]; }; 与 memcpy),则多个加载或存储将位于 same 缓存行。这很便宜(如果您之前没有在指令或 L1D 吞吐量上遇到瓶颈)并且不会导致额外的缓存未命中,但会破坏自动矢量化,因此您甚至可能无法跟上内存的顺序访问。 (通常,您希望它在支持未对齐加载的目标上编译为 32 位 + 8 位加载。现代 x86 对缓存行拆分的惩罚较低,尽管当字加载拆分到 4k 页时,惩罚更高)。
    • 包含分支的打包/解包策略是可能的,但几乎不值得一提,除非您使用uintptr_t 和对齐检查/宽负载 (like you might consider in asm) 手动获取超低级别。或者您是在谈论在uint64_t [] 之上执行此操作并使用if 来确定您是否只需要一个负载?与仅使用移位将 uint64_t 与 uint32_tuint8_t 拆分/合并,并使用 memcpy 或使用结构进行对齐分组相比,这听起来是个坏主意。
    • 根据 ISO C++11,您可以使用零宽度位域分隔“内存位置”。我不确定标准是否暗示struct __attribute__((packed)) { unsigned long long v:40; }; 的数组真的是一个巨大的内存位置;但即使结构边界不是内存位置边界,您也可以使用 int end:0 来保证(modulo compiler bugs! 和 stackoverflow.com/questions/47008183/…
    【解决方案13】:

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

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

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

    【讨论】:

      【解决方案14】:

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

      【讨论】:

      • 这不会对缓存造成可怕的影响吗?
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多