【问题标题】:Reduce memory footprint in the construction of a BigInt class减少构建 BigInt 类的内存占用
【发布时间】:2018-11-08 08:58:24
【问题描述】:

我目前正在研究一个概念,并且在我提出这个问题时正在编写伪代码。我正在考虑制作一个相当简单易用的类接口来表示 BigInts。我正在考虑制作几个具有 BigInt 类将使用的基本属性和成员的简单结构。例如,它不是直接处理负值的 BigInt 类,而是包含一个 Sign 结构,并且这个结构基本上包含一个值 0 或 1,或者基本上是一个布尔类型来指定这个 BigInt 是正数还是负数。在构建时,我打算让该类默认生成一个正数。我还想有一个结构来表示有两个变体的数字。第一个变体具有数字 0-9,第二个将继承原始变体,但也包括 A-F。这样,作为模板类但只有两种有效类型的类将支持十进制和十六进制的使用。所有数学运算符都将在类之外定义,并且根据其推断类型,它将调用并执行适当版本的函数。然而,十六进制部分仍然只是概念,因为我想首先启动并运行十进制版本。这些帮助类可能看起来像这样:

class Sign {
private:
    bool isNegative_ { false };
    char sign_;
public:
    Sign() : isNegative_( false ) {
        sign_ = '+';
    }
    Sign( const bool isNegative ) : isNegative_( isNegative ) { 
        sign_ = isNegative_ ? '-' : '+';
    }; 

    Sign( const char sign ) {
        if ( sign == '+' || sign == '\0' || sign == ' ' ) {
            isNegative_ = false;
        } else if ( sign == '-' ) {
            isNegative_ = true;
        } else {
            // throw error improper character.
        }
    }

    bool isPostive() const { return !isNegative_; }
    bool isNegative() const { return !isNegative; }

    char sign() const { return sign_; }
    char negate() {
        isNegative_ = !isNegative_;
        sign_ = isNegative_ ? '+' : '-'; 
        return sign_;         
    }        
};

// NST = NumberSystemType
class enum NST { Decimal = 10, Hexadecimal = 16 };

template<class NT> // NT = NumberType
class Digit {
private:
    NST nst_; // determines if Decimal or Hexadecimal       
};

// Specialization for Decimal
template<NST::Decimal> // Should be same as template<10>
class Digit {
    // would like some way to define 4 bits to represent 0-9; prefer not to
    // use bitfields, but I think a char or unsigned char would be wasting 
    // memory using twice as much for what is needed. Not sure what to do here...
    // maybe std::bitset<>...


};

template<NST::Hexadecimal> // Should be same as template<16>
class Digit : public Digit<Decimal> { // Also inherits all of The first specialization.
    // same as above only needs 4 bits to represent values form 0-F
    // A half char would be excellent but does not exist in c++... and most
    // programming language's types.
    // still thinking of maybe std::bitset<>...

};

两者之间的主要区别在于,第一个特化只允许 0-9 的数字值和 0-9 的数字本身,而第二个没有这个限制,但也允许从 a-f 和或 A-F 任一种情况已验证。我还可以包含一个 const char* 来指定 0x 的十六进制前缀,它将附加到任何包含的值以进行显示。

我喜欢这种设计方法,因为我希望将 BigInt 类的实际算术函数和运算符保留为单独的函数模板,因为 BigInt 类可以支持 Decimal 和 Hexadecimal 专用模板类型。如果一切顺利,我还想添加支持以处理复数。

BigInt 类应该是这样的:

template<class NT>
BigInt {
private:
    Sign sign_;
    Digit<NT> carryDigit_;
    std::vector<Digit<NT>> value_;

    // May contain some functions such as setters and getters only
    // Do not want the class to be modifying itself except for assignment only.
};

如上所述,这也适用于十进制和十六进制类型,但是如果有人创建了 BigInt myBigInt 的实例,则默认为十进制!

对于包含在向量中的数据。我想以与阅读内容相反的顺序存储数字。因此,如果它们是 BigInt 内部向量中的数字345698,它将被存储为896543。这样做的原因是,当我们在数学中进行算术运算时,我们从小数点左侧的右侧开始从最低有效位到最高有效位,这是无关紧要的,因为这是一个仅限 BigInt 的类,我们以自己的方式工作左边。但是,如果我们以正确的顺序在上述类的向量的每个元素中存储每个只能是 0-9 的数字,并且我们使用外部 operator+() 函数,这对于一个 BigInt 到另一个来说将是具有挑战性的......例如:

Basic Arithmetic R - L    | Vector storing standard
12345                       <1,2,3,4,5>
+ 678                       <6,7,8>
------  
13023

这里 和 的索引不重合,所以这使得很难弄清楚如何将一个有几个数字的值添加到一个有多个数字。我的方法是,如果我们以相反的顺序存储数字:

                         | Vector stored in reverse
                           <5,4,3,2,1>
                           <6,7,8>

那么加法就变得简单了!我们所要做的就是将 BigInt 的两个向量中的每个数字加上相同的索引值。我们可以使用进位数字来结转到下一个字段。生成的 BigInt 返回的大小至少等于或大于两个 BigInt 中最大的一个。如果 carryDigit 有一个值,那么下一次迭代的加法运算将包括 3 个加法而不是 2 个。现在,当获取 BigInt 进行显示时,我们可以返回一个向量>,但当用户获取它时,这不是“BigInt”,它是数字向量,并且它的顺序也是正确的。它甚至可以通过字符串表示形式返回。这个类可以通过一个vector来构造>,在内部存储的时候会颠倒顺序。

这是我的 BigInt 类的总体概念,我只是想知道这是否是一个好的设计计划,是否会被认为是有效的,我想我的主要问题是关于使用什么来存储实际的Digit 类中的数字...std::bitset&lt;&gt; 是否适合节省内存占用空间,或者使用char 会更好,而不用担心额外的空间,因为它更易于实现?

【问题讨论】:

  • 这是作为练习还是制作?因为那里有一大堆经过验证的真正的 bigint 库,带有 C++ 接口,例如GMP.
  • 如果我要实现自己的大整数,我会使用内置类型(例如 32 位)来保存每个“数字”......本质上意味着我的数字表示是基本的 - 2 ^ 32,而不是你建议的base-10或base-16。我所有的操作都是二进制的,不会浪费空间。所以整个数字就像std::vector&lt;uint32_t&gt;。这使得 Input/Output 与 base-10 之间的转换稍微复杂一些,但数学运算会很快且可矢量化……对我来说,这就是 big-int 的全部意义。
  • "半个字符会很好,但在 c++ 中不存在..." 它也不存在于汇编程序中,因为它对于设计用于处理 32 位或 64 位的处理器毫无意义一次处理 8 位的内部便利。而std::bitset 不会帮助你,因为它以字大小的块分配存储(至少在 GCC 实现中)。
  • 嗯,在我看来,您已经看到了问题,选择了不合适的解决方案,然后对其进行了过度设计。
  • @FrancisCugler - 我同意帕迪的观点。十六进制和十进制的不同类型是非常有缺陷的设计。整数类型的一项要求是能够相互分配和比较它们(例如相等)。您的方法意味着您需要实施的操作数量与您支持的基础数量成二次方 - 难以维护。考虑使用单个类来有效地表示一个无符号的 bigint 值,然后使用该无符号的 bigint 类型和一个 bool 来实现一个有符号的 bigint 来表示符号。在输入或输出处处理碱基,而不是在内存中具有不同的表示。

标签: c++ types biginteger template-specialization memory-footprint


【解决方案1】:

C++ 中最小的可寻址内存单元是一个字节,而一个字节至少是 8 位(通常正好是 8 位)。所以你的想法确实是浪费内存。

我还怀疑你对数学不太熟悉。你所说的“NumberSystemType”通常被称为base。所以我们谈论base-10或base-16。

现在 base-10 仅对人类消费有用,因此对于这种 bigints 毫无意义。无论如何,人类无法处理超过 20 位十进制数字的数字。所以,选择一个对计算机有用的基础。并选择一个。无需支持多个基地。正如 paddy 指出的那样,base 2^32 是计算机的一个完全合理的基础。

【讨论】:

  • 我理解你所说的以 10 为底和以 16 为底的对数的区别。我只是给它一个友好易读的名称,因为十进制的 base-10 是一个数字系统,而十进制的 base-16 也是一个数字系统。我试图只为每个元素 0-9 存储在 BigInt 容器的十进制版本中,除了十六进制版本,它将存储 0-F。原因是在编写函数来执行加法 - 减法等时,如果数字以相反的顺序存储在向量中,它们会变得非常容易!
  • @FrancisCugler:哦,倒序存储数字的想法还不错。但是,当您选择一个合理的基数(如 2^8 或 2^32)时,这仍然有效。选择像 2^4 这样太小的基数只会增加问题而没有任何好处。
  • @FrancisCugler:是的,我已经怀疑你错过了必要的数学基础。 &lt; 9, 2334343, 235, 3, 243 &gt; 是基数 2^32 中的 5 个非常不错的数字。当然,那里没有数字“03”。如果有的话,那就是0x00000003
  • 回答上面的问题:是的,也许是(例如,如果你乘以 64 位,或者使用存储溢出的特殊指令,这对这些场景很有用)。这就是我建议您使用 base-256 的原因之一,直到您开始了解如何管理诸如 32 位整数中的溢出之类的事情。这里有一个明显的知识差距,很难以任何形式回答您的问题。您的主要问题是需要节省空间。我们已经多次告诉您如何实现这一目标,剩下的就是让您去尝试,并在此过程中学习。
  • 是的,这正是我们所说的。
【解决方案2】:

-更新-

好的,我接受了一些建议,这就是我目前所拥有的。我还没有包含以相反顺序存储值的概念,这应该不会太难。就目前而言,我已经消除了所有外部类,我的 BigInt 类签名如下所示:

class BigInt {
private:
    short sign_;
    uint32_t carry_{0};
    std::vector<uint32_t> value_;

public:
    BigInt() : sign_( 0 ) {}

    BigInt(  const uint32_t* value, const std::size_t arraySize, short sign = 0 ) : 
    sign_( sign ) {
        if( sign_ != 0 ) sign_ = -1;
        value_.insert( value_.end(), &value[0], &value[arraySize] );
        std::reverse( value_.begin(), value_.end() );
    }

    BigInt( const std::vector<uint32_t>& value, short sign = 0 ) : 
    sign_( sign ) {
        if( sign_ != 0 ) sign_ = -1;
        value_.insert( value_.end(), value.begin(), value.end() );
        std::reverse( value_.begin(), value_.end() );
    }

    BigInt( std::initializer_list<uint32_t> values, short sign = 0 ) : 
    sign_( sign ) {
        if( sign_ != 0  ) sign_ = -1;
        value_.insert( value_.end(), values.begin(), values.end() );
        std::reverse( value_.begin(), value_.end() );
    }

    std::vector<uint32_t> operator()() {
        //value_.push_back( static_cast<uint32_t>( sign_ ) ); // causes negative to be pushed into container multiple times...
        std::reverse( value_.begin(), value_.end() );
        return value_;
    }
};

我认为这是一个好的开始:有几种方法可以构造一个给调用者一些灵活性的方法,因为您可以使用指针、数组、向量甚至初始化列表创建 BigInt。起初,在构造函数的末尾有符号似乎是反直觉的,但我希望能够在值为正时将符号值作为默认参数。如果为符号值传入除0 以外的任何内容,它将转换为-1。再多一点时间,我将完成这个类的签名,然后我可以转到将处理它的运算符。

上面的代码有一些小缺陷,但我一直在努力。

【讨论】:

  • 最小的可寻址存储单元是char。如果您坚持将您的“数字”类型模板化为以 10 为底的整数类型,那么您要么需要接受浪费大量位,要么将您的数字打包为 BCD(即每个字节两位数)并成对索引它们.如果您的数学是轻量级的,并且主要处理要求是转换为/从基数 10 转换,那么继续。如果主要的处理要求是快速的数学运算和紧凑的存储,那么您就走错了路,您应该使用可以打包成原生整数类型的 2 次幂的基数。
  • @paddy 我用工作代码更新了我的答案:它还不完整且没有错误,但它正在进行中。这个类基本上只存储内容,管理其元素排序和检查有效输入。我计划在与此类成为朋友的外部函数中实现所有关于此的操作。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2014-04-07
  • 2016-11-09
  • 1970-01-01
  • 1970-01-01
  • 2021-07-21
  • 2012-08-25
  • 1970-01-01
相关资源
最近更新 更多