【问题标题】:Is there a 256-bit integer type?有 256 位整数类型吗?
【发布时间】:2019-09-12 02:43:04
【问题描述】:

操作系统:Linux (Debian 10)

CC:GCC 8.3

CPU:i7-5775C

在 GCC 中有一个 unsigned __int128/__int128,但是有什么方法可以在 GCC 中有一个 uint256_t/int256_t

我读过似乎来自英特尔的__m256i。有没有我可以包含的标题来获取它?

它和假设的unsigned __int256 一样有用吗?我的意思是,如果您可以对其进行分配、比较、按位运算等。

它的签名等价物是什么(如果有的话)?


编辑 1:

我做到了:

#include <immintrin.h>
typedef __m256i uint256_t;

并编译。如果我可以用它做一些操作,我会在这里更新它。


编辑 2:

发现的问题:

uint256_t   m;
int         l = 5;

m = ~((uint256_t)1 << l);

输出:

error: can’t convert a value of type ‘int’ to vector type ‘__vector(4) long long int’ which has different size
  m = ~((uint256_t)1 << l);

【问题讨论】:

标签: c gcc x86-64 bigint extended-precision


【解决方案1】:

Clang 有 _ExtInt extended integers 支持除除以外的运算,但 SIMD 对此没有用处,因为元素之间存在进位1。其他主流 x86-64 编译器甚至没有。您需要一个库或其他东西来定义自定义类型并使用 clang 将使用的相同 add-with-carry 指令。 (或纯 C2 中效率较低的仿真)。

__m256i 是 AVX2 SIMD 4x uint64_t(或更窄的元素大小,如 8x uint32_t)。 它不是 256 位标量整数类型,不能用于标量运算,__m256i var = 1 甚至无法编译。 x86 SIMD 不支持大于 64 位的整数,而英特尔内部类型(如 __m128i__m256i)纯粹用于 SIMD。

GCC 的 __int128 / unsigned __int128 通常使用标量 add/adc 和/或标量 mul / imul,因为 AVX2 通常对扩展精度没有帮助。 (仅适用于元素边界无关的按位 AND/OR/XOR。)


脚注 1:实际上对于 BigInteger 类型使用 SIMD 是有一定范围的,但只能使用专门的格式。更重要的是,您必须手动选择何时重新规范化(传播进位),因此您的计算必须围绕它进行设计;它不是直接替代品。在 Can long integer routines benefit from SSE? 上查看 Mysticial 的回答

脚注 2:不幸的是,C 不提供加法/减法的进位,所以用 C 编写甚至不方便。sum = a+b / carry = sum&lt;a 用于在没有时执行进位,但是用 C 编写一个完整的加法器要困难得多。而且编译器通常会制作垃圾 asm,而不仅仅是在可用的机器上使用本机 add-with-carry 指令。用于非常大整数的扩展精度库,例如 GMP,通常用 asm 编写。

【讨论】:

  • 但是我不能在我的代码中使用__m256i 吗?我的意思是像__m256i var1 = 0x7u; __m256i var2 = 0x8u; __m256i var3 = var2 &amp; var1;
  • @CacahueteFrito no,__m256i 用于 AVX2,它不是单个 256 位整数
  • @CacahueteFrito __m256i 基本上是 8 x 32 位整数(以及其他一些变体)。这 8 个整数基本上是独立的变量。混合和匹配这些整数(如您需要的 add-with-carry)会带来性能损失。这种类型的 2x 128 位通道上的通信更加昂贵。您需要找到一个使用标准 64 位/32 位整数类型处理大整数的 C++ 库。
  • "您需要一个使用 add-with-carry 的库。"您需要一个库,但它的实现方式并不重要。 IIRC RISCV 没有 add-with-carry,但您仍然可以在其上模拟 256 位整数类型。
  • @MarcGlisse:如果它本身没有 add-with-carry,则必须模拟它。但是你确实需要 add-with-carry,除非你有一个原生的 256 位类型。
【解决方案2】:

我只在 Pollard Rho 算法中计算“f(x) = (x^2+a) mod n”时才需要“uint256_t”。函数“f”之外的所有变量都是内置类型 __uint128_t。

我为此目的实现了 uint256_t,如下所示:

typedef __uint128_t uint256_t[2];

然后我实现了计算“f()”所需的函数:

__uint128_t set_128(unsigned long h, unsigned long l);
void set_256(uint256_t d, __uint128_t l, __uint128_t h);
void add_128(uint256_t d, uint256_t x, __uint128_t a);
void add_256(uint256_t d, uint256_t x, uint256_t a);
void shl_256(uint256_t d, long s);
void sqr_128(uint256_t d, __uint128_t x);
several print functions and macros for printing 128bit and 256bit numbers
__uint128_t mod_256(uint256_t x, __uint128_t n);
__uint128_t f(__uint128_t x);

在这个 gist 中找到实现:
https://gist.github.com/Hermann-SW/a20af17ee6666467fe0b5c573dae701d

我确实针对 gmplib 函数对我的代码进行了基准测试,并实现了对所有 gmplib 的加速(经过大量工作),详情:
https://www.raspberrypi.org/forums/viewtopic.php?f=33&t=311893&p=1873552#p1873552

执行 100 万次函数的运行时间(以纳秒为单位):

【讨论】:

  • 对于显示普通__uint128_t 的 155k 加速因子的基准测试,很可能大部分工作已被优化掉或被吊出循环,但 gmp 函数调用对优化器是不透明的.现代 Intel/AMD CPU 的循环速度不可能超过每个时钟周期 1 次迭代,或者每个时钟执行超过 1 个标量 64x64 => 128 位 mul。如果您发现您的代码运行得比这更快,那么它实际上已经优化了。 (4GHz 下的 147 ns 仅 588 个周期,如果没有大量算法优化,即不做所有工作,做一百万次任何事情是完全不可能的)
  • 查看例如 Google Benchmark 的 DoNotOptimize() 函数,以使编译器忘记它对值的了解。问答:Preventing compiler optimizations while benchmarking / Google Benchmark Frameworks DoNotOptimize
  • 您的uint256_t 对 GMP 的加速确实看起来更合理:特别是对于平方,三个乘法和一些加法与 GMP 的可变长度循环的一般情况相比并没有太多工作。不过,我仍然很惊讶它竟然有 20 个。也许一些工作仍然被优化掉了,或者如果你不直接使用mpn_函数,可能会有更多的GMP开销。
  • 你是对的,编译器完全优化了乘法:godbolt.org/z/584f4e9o4我在总和变量“s”前加上了volatile,汇编代码完全改变了:godbolt.org/z/WdnG458E1__uint128_t 乘法在gmplib上的加速“mpz_mul( )" 现在是 14694553/1497909 = 9.8 倍。
  • volatile 可能会造成比必要的伤害更多,强制额外的负载以及阻止编译器注意到a_high * a_lowa_low * a_high 的计算相同,如果您不会对其进行特殊处理,而是依靠编译器在内联两个 args 相同的正常乘法函数时注意到冗余。或者,每次您提到a 时,都会从所有额外的负载中获得额外的工作。微基准测试很难:您必须检查 asm 以查看循环中的工作是否正是您想要测量的。 (虽然 DoNotOptimize 可以提供很多帮助。)
猜你喜欢
  • 2011-07-11
  • 2015-08-30
  • 2015-01-15
  • 2016-03-23
  • 1970-01-01
  • 2015-01-11
  • 2018-09-09
  • 2012-04-17
  • 1970-01-01
相关资源
最近更新 更多