使用 AVX2 向量化随机初始化并使用十进制数字数组打印 BigInt?

2023-12-12

如何将我的代码传递给 AVX2 代码并获得与以前相同的结果?

是否可以使用__m256i在 LongNumInit 中,LongNumPrint 函数代替uint8_t *L,或某种类似类型的变量?

我对 AVX 的了解相当有限;我调查了很多,但是我不太明白如何转换我的代码,欢迎任何建议和解释。

我对 AVX2 中的这段代码非常感兴趣。

void LongNumInit(uint8_t *L, size_t N )
{
  for(size_t i = 0; i < N; ++i){
      L[i] = myRandom()%10;
  }

}
void LongNumPrint( uint8_t *L, size_t N, uint8_t *Name )
{
  printf("%s:", Name);
  for ( size_t i=N; i>0;--i )
  {
    printf("%d", L[i-1]);
  }
  printf("\n");
}
int main (int argc, char **argv)
{
  int i, sum1, sum2, sum3, N=10000, Rep=50;

  seed = 12345;

  // obtain parameters at run time
  if (argc>1) { N    = atoi(argv[1]); }
  if (argc>2) { Rep  = atoi(argv[2]); }
  
 // Create Long Nums
  unsigned char *V1= (unsigned char*) malloc( N);
  unsigned char *V2= (unsigned char*) malloc( N);
  unsigned char *V3= (unsigned char*) malloc( N);
  unsigned char *V4= (unsigned char*) malloc( N);

  LongNumInit ( V1, N ); LongNumInit ( V2, N ); LongNumInit ( V3, N );
   
//Print last 32 digits of Long Numbers
  LongNumPrint( V1, 32, "V1" );
 LongNumPrint( V2, 32, "V2" );
  LongNumPrint( V3, 32, "V3" );
  LongNumPrint( V4, 32, "V4" );

  free(V1); free(V2); free(V3); free(V4);
  return 0;
}

我在初始代码中获得的结果是这样的:

V1:59348245908804493219098067811457
V2:24890422397351614779297691741341
V3:63392771324953818089038280656869
V4:00000000000000000000000000000000

一般来说,对于 BigInteger 来说这是一种糟糕的格式,请参阅https://codereview.stackexchange.com/a/237764对 BigInteger 每字节使用一位十进制数字的设计缺陷进行代码审查,以及您可以/应该做什么。

And see 长整数例程可以从 SSE 中受益吗?@Mysticial 关于存储数据的方法的注释,这些数据使 SIMD for BigInteger 数学变得实用,特别是部分字算术,其中您的临时变量可能不会“标准化”,让您可以进行惰性进位处理。


但显然你只是问this代码,随机初始化和打印函数,而不是如何在这种格式的两个数字之间进行数学运算。

我们可以很好地对这两者进行矢量化。我的LongNumPrintName()是您的直接替代品。

For LongNumInit我只是展示一个存储两个 32 字节块并返回一个递增指针的构建块。循环调用它。 (每次调用它自然会生成 2 个向量,因此对于较小的 N,您可以制作一个替代版本。)

LongNumInit

生成包含随机数字的 1 GB 文本文件的最快方法是什么?在 4GHz Skylake 上以大约 33 GB/s 的速度生成空格分隔的随机 ASCII 十进制数字,包括write()系统调用/dev/null。 (这高于 DRAM 带宽;128kiB 的缓存阻塞让存储命中 L2 缓存。内核驱动程序/dev/null甚至不读取用户空间缓冲区。)

它可以很容易地改编成 AVX2 版本void LongNumInit(uint8_t *L, size_t N )。我的答案是使用 AVX2 xorshift128+ PRNG(在 64 位元素中使用 4 个独立的 PRNG 进行矢量化)__m256i) like xorshift128+ 的 AVX/SSE 版本。这应该与你的随机性质量相似rand() % 10.

它通过乘法逆元将其分解为十进制数字,并通过移位和模除以 10vpmulhuw, using 为什么GCC在实现整数除法时使用乘以奇怪的数字?。 (实际上使用 GNU C 本机向量语法让 GCC 确定魔术常数并发出乘法和移位以方便语法,例如v16u dig1 = v % ten; and v /= ten;)

您可以使用_mm256_packus_epi16将两个 16 位数字向量打包成 8 位元素,而不是将奇数元素转换为 ASCII' '将偶数元素转换为 ASCII'0'..'9'。 (所以改变vec_store_digit_and_space用常量打包向量对而不是 O'Ring,见下文)

使用 gcc、clang 或 ICC(或者希望任何其他能够理解 C99 的 GNU C 方言和 Intel 内在函数的编译器)来编译它。

See https://gcc.gnu.org/onlinedocs/gcc/Vector-Extensions.html为了__attribute__((vector_size(32)))部分,以及https://software.intel.com/sites/landingpage/IntrinsicsGuide/为了_mm256_*东西。还https://stackoverflow.com/tags/sse/info.

#include <immintrin.h>

// GNU C native vectors let us get the compiler to do stuff like %10 each element
typedef unsigned short v16u __attribute__((vector_size(32)));

// returns p + size of stores.  Caller should use outpos = f(vec, outpos)
// p must be aligned
__m256i* vec_store_digits(__m256i vec, __m256i *restrict p)
{
    v16u v = (v16u)vec;
    v16u ten = (v16u)_mm256_set1_epi16(10);

    v16u divisor = (v16u)_mm256_set1_epi16(6554);  // ceil((2^16-1) / 10.0)
    v16u div6554 = v / divisor;      // Basically the entropy from the upper two decimal digits: 0..65.
    // Probably some correlation with the modulo-based values, especially dig3, but we do this instead of
    // dig4 for more ILP and fewer instructions total.

    v16u dig1 = v % ten;
    v /= ten;
    v16u dig2 = v % ten;
    v /= ten;
    v16u dig3 = v % ten;
    //  dig4 would overlap much of the randomness that div6554 gets

    // __m256i or v16u assignment is an aligned store
    v16u *vecbuf = (v16u*)p;
      // pack 16->8 bits.
    vecbuf[0] = _mm256_packus_epi16(div6554, dig1);
    vecbuf[1] = _mm256_packus_epi16(dig2, dig3)
    return p + 2;  // always a constant number of full vectors
}

逻辑在random_decimal_fill_buffer插入换行符的操作可以完全删除,因为您只需要一个由十进制数字组成的平面数组。只需循环调用上述函数,直到填满缓冲区。

处理小尺寸(小于完整向量):

将 malloc 填充到下一个 32 字节的倍数会很方便,因此执行 32 字节加载始终是安全的,而无需检查是否可能跨越到未映射的页面。

并使用C11aligned_alloc获得 32 字节对齐的存储。例如,aligned_alloc(32, (size+31) & -32)。即使 N 是奇数,这也让我们可以进行完整的 32 字节存储。从逻辑上讲,只有缓冲区的前 N ​​个字节保存了我们的真实数据,但是我们可以方便地进行填充,以避免对 N 小于 32 或不是 32 的倍数进行任何额外的条件检查。

不幸的是 ISO C 和 glibc 丢失了aligned_realloc and aligned_calloc。 MSVC 实际上提供了这些:为什么大多数平台上没有“aligned_realloc”?允许您有时在对齐缓冲区的末尾分配更多空间而不复制它。 “try_realloc”对于 C++ 来说是理想的选择,如果不可复制的对象更改地址,则可能需要运行复制构造函数。非表达性的分配器 API 会强制执行有时不必要的复制,这是我最讨厌的事情。


LongNumPrint

采取uint8_t *Namearg 是糟糕的设计。如果调用者想要 printf a"something:"先串起来,他们可以做到。你的函数应该做什么printf "%d"确实对于一个int.

由于您以相反的打印顺序存储数字,因此您需要将字节反转到 tmp 缓冲区并将 0..9 字节值转换为'0'..'9'通过 ORing 得到的 ASCII 字符值'0'。然后将该缓冲区传递给fwrite.

具体来说,使用alignas(32) char tmpbuf[8192];作为局部变量。

您可以使用固定大小的块(例如 1kiB 或 8kiB),而不是分配可能巨大的缓冲区。您可能仍然想通过 stdio (而不是write()直接管理您自己的 I/O 缓冲)。具有 8kiB 缓冲区,高效fwrite可能只是将其传递给write()直接而不是 memcpy 到 stdio 缓冲区中。您可能想尝试对此进行调整,但保持 tmp 缓冲区小于 L1d 缓存的一半将意味着在写入后重新读取它时,它在缓存中仍然很热。

缓存阻塞使循环边界变得更加复杂,但对于非常大的 N 来说这是值得的。

一次反转 32 个字节:

您可以通过决定您的数字以 MSD 优先顺序存储来避免这项工作,但如果您确实想实现加法,则必须从末尾向后循环。

您的功能可以用 SIMD 实现_mm_shuffle_epi8反转 16 字节块,从数字数组的末尾开始写入 tmp 缓冲区的开头。

或者更好,加载vmovdqu / vinserti12816 字节负载供给_mm256_shuffle_epi8在通道内进行字节反转,设置 32 字节存储。

在英特尔 CPU 上,vinserti128解码为加载+ALU uop,但它可以在任何向量 ALU 端口上运行,而不仅仅是 shuffle 端口。因此两个 128 位加载比 256 位加载更高效 ->vpshufb - > vpermq如果缓存中的数据很热,这可能会成为洗牌端口吞吐量的瓶颈。 Intel CPU 每个时钟周期最多可以执行 2 次加载 + 1 次存储(或者在 IceLake 中,2 次加载 + 2 次存储)。如果没有内存瓶颈,我们可能会在前端遇到瓶颈,因此实际上不会使加载+存储和洗牌端口饱和。 (https://agner.org/optimize/ and https://uops.info/)

这个函数也被简化了,假设我们总是可以读取 32 个字节L不会进入未映射的页面。但是,在对小 N 进行 32 字节反转之后,输入的前 N ​​个字节将成为 32 字节块中的最后 N 个字节。如果我们总是能够安全地进行 32 字节加载,那将是最方便的ending在缓冲区的末尾,但期望在对象之前进行填充是不合理的。

#include <immintrin.h>
#include <stdalign.h>
#include <stddef.h>
#include <stdio.h>
#include <stdint.h>

// one vector of 32 bytes of digits, reversed and converted to ASCII
static inline
void ASCIIrev32B(void *dst, const void *src)
{
    __m128i hi = _mm_loadu_si128(1 + (const __m128i*)src);  // unaligned loads
    __m128i lo = _mm_loadu_si128(src);
    __m256i v = _mm256_set_m128i(lo, hi);    // reverse 128-bit hi/lo halves

    // compilers will hoist constants out of inline functions
    __m128i byterev_lane = _mm_set_epi8(0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15);      
    __m256i byterev = _mm256_broadcastsi128_si256(byterev_lane);  // same in each lane
    v = _mm256_shuffle_epi8(v, byterev);               // in-lane reverse

    v = _mm256_or_si256(v, _mm256_set1_epi8('0'));     // digits to ASCII
    _mm256_storeu_si256(dst, v);                       // Will usually be aligned in practice.
}

// Tested for N=32; could be bugs in the loop bounds for other N
// returns bytes written, like fwrite: N means no error, 0 means error in all fwrites
size_t LongNumPrint( uint8_t *num, size_t N)
{
    // caller can print a name if it wants

    const int revbufsize = 8192;      // 8kiB on the stack should be fine
    alignas(32) char revbuf[revbufsize];

    if (N<32) {
        // TODO: maybe use a smaller revbuf for this case to avoid touching new stack pages
        ASCIIrev32B(revbuf, num);   // the data we want is at the *end* of a 32-byte reverse
        return fwrite(revbuf+32-N, 1, N, stdout);
    }

    size_t bytes_written = 0;
    const uint8_t *inp = num+N;  // start with last 32 bytes of num[]
    do {
        size_t chunksize = (inp - num >= revbufsize) ? revbufsize : inp - num;

        const uint8_t *inp_stop = inp - chunksize + 32;   // leave one full vector for the end
        uint8_t *outp = revbuf;
        while (inp > inp_stop) {        // may run 0 times
            inp -= 32;
            ASCIIrev32B(outp, inp);
            outp += 32;
        }
        // reverse first (lowest address) 32 bytes of this chunk of num
        // into last 32 bytes of this chunk of revbuf
        // if chunksize%32 != 0 this will overlap, which is fine.
        ASCIIrev32B(revbuf + chunksize - 32, inp_stop - 32);
        bytes_written += fwrite(revbuf, 1, chunksize, stdout);
        inp = inp_stop - 32;
    } while ( inp > num );

    return bytes_written;
    // caller can putchar('\n') if it wants
}


// wrapper that prints name and newline
void LongNumPrintName(uint8_t *num, size_t N, const char *name)
{
    printf("%s:", name);
    //LongNumPrint_scalar(num, N);
    LongNumPrint(num, N);
    putchar('\n');
}

// main() included on Godbolt link that runs successfully

这将编译并运行(在戈德螺栓上) with gcc -O3 -march=haswell并为 N=32 的标量循环产生相同的输出main通过。 (我用了rand()代替MyRandom(),因此我们可以使用您的 init 函数使用相同的种子进行测试并获得相同的数字。)

未经测试较大的 N,但 chunksize = min(ptrdiff, 8k) 的一般思想并使用它从末尾向下循环num[]应该是固体的。

如果我们转换第一个,我们可以加载(而不仅仅是存储)对齐向量N%32字节并将其传递给fwrite在开始主循环之前。但这可能会导致额外的write()系统调用,或在 stdio 内进行笨重的复制。 (除非已经有缓冲的文本尚未打印,例如Name:,在这种情况下我们已经受到了惩罚。)

请注意,从技术上讲,C UB 是递减的inp过去的开始num. So inp -= 32代替inp = inp_stop-32将有用于离开外循环的迭代的 UB。我实际上在这个版本中避免了这种情况,但无论如何它通常都是有效的,因为我认为 GCC 假设一个平面内存模型,并且 de-factor 定义了指针比较的行为。普通操作系统保留零页,因此num绝对不能在物理内存开头的 32 个字节内(所以inp无法换行到高地址。)这一段主要是第一次完全未经测试的尝试留下的,我认为该尝试在内循环中将指针递减得比实际情况更远。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

使用 AVX2 向量化随机初始化并使用十进制数字数组打印 BigInt? 的相关文章

随机推荐

  • 并行触发异步请求,但使用 rxjs 按顺序获取结果

    例如 使用 jquery ajax 并行获取 5 个页面 当第 2 页返回时 不执行任何操作 当 page1 返回时 对 page1 和 page2 执行一些操作 assume there is some operator that can
  • Android 列表视图中的 OnItemClickListener 与 OnclickListener

    由于代码的原因 这会很长 所以我有一个关于 OnItemclickListener 与 OnclickListener 的问题 这里我有两个代码 每个代码都有效 所以是否有优先使用这两个代码之一或者我可以使用任何人 这是 OnItemCli
  • 如何让我的动画更加流畅 Android

    我有一个应用程序 有一个球在屏幕上运行 当球到达一半时 应用程序会记录一些音频 计算 FFT 并进行一些额外的分析 这是由 Asynctask 处理的 但是动画仍然短暂地卡顿 有人对如何使其运行更流畅有任何建议吗 Thanks 代码如下 i
  • Protractor - 在启动我的测试规范之前执行登录脚本

    这里的基本问题是我最初尝试登录 启动我的应用程序 然后运行所有规范 事实证明NOT是一个好方法 我无法弄清楚的是 Why navpanel spec js下面首先运行 在登录和启动 js 文件之前 换句话说 如果我在 navpanel sp
  • 有没有办法在android平台上自动播放视频?

    我读了数十亿个关于堆栈溢出的论坛和帖子 但什么也没有 当我找到解决方案或类似的东西时 就不再工作了 我尝试使用 Google API 但什么也没有 我可以在 IOS 本机播放器中自动播放视频 将直接 mp4 链接放在标签 上 但在 Andr
  • 如何使用JavaScript控制音频元素

    首先 我尝试不使用默认的 html5 标准控件 如果可能的话我很乐意使用 jQuery 但我暂时将其删除 因为我不确定问题是什么 目前我只是想有一个播放按钮 单击后会播放一 些音乐 该按钮将更改为暂停按钮 一旦点击这个暂停按钮 音乐就会明显
  • BottomNavigationView 不存在

    我正在尝试将 navigationeditor 与底部导航视图一起使用 但似乎底部导航视图只是导致问题的原因 这是我的 xml
  • 将平面数组中的每个字符串复制 N 次

    我想将每个值重复 3 次并按正确的顺序排列 应使用其自身的 3 个副本来代替每个原始元素 给定以下一维字符串数组 chars a b c 所以结果是 duplicatedChars a a a b b b c c c 我尝试过与str re
  • Gradle 如何从 apk 中排除文件

    我将密钥库存储在资产目录中 如何在构建中排除它以创建 apk 我以这种方式尝试过 但仍然存在 android packagingOptions exclude META INF LICENSE txt exclude assets keys
  • 如何向 EF Core 中的所有实体添加相同的列?

    想象一下 我想向我的所有实体添加 IsDeleted 列或一些审核列 我可以创建一个基类 我的所有实体都将继承该基类 这将解决我的问题 但是我无法指定创建列的顺序 因此我最终会在实体的字段之前得到所有审核字段 这是我不想要的 我希望他们位于
  • 程序最小化后无法从任务栏检索

    我将提供一些关于我正在尝试做的事情的背景 我创建了一个自定义按钮 该按钮应该通过淡出动画最小化我的窗口 因此它的代码如下 private void minimize Window object sender EventArgs e var
  • 同一变量“args”有两个不同的值

    我正在从 python 脚本调用一个方法 其中一个变量作为 args 一旦我进入该方法 当我尝试查看变量 args 的值时 print args 并且仅执行 args 会显示两个不同的值 谁能告诉我这两个命令有什么区别 我希望这两个命令显示
  • javascript大整数舍入是因为精度? (为什么?)

    如果你这样做 for var i 0 i lt 30 i console log i 78764357878563800 console log 78764357878563790 i 78764357878563800 您开始比较从 78
  • GNU make 似乎忽略了中间文件的非终端匹配规则

    我的目录中有以下文件 FP01 c include
  • 禁用有关在派生类的复制构造函数内显式初始化基构造函数的警告

    我正在使用启用了 Wextra 的 g 版本 4 2 1 我包含来自库的标头 并且不断收到有关库中某个类的以下警告 该警告由 Wextra 启用 我已将类的实际名称替换为 BaseClass warning base class class
  • 找到 setTimeout() 中剩余的时间?

    我正在编写一些与我不拥有的库代码交互的Javascript 并且无法 合理地 更改 它创建 Javascript 超时 用于显示一系列限时问题中的下一个问题 这不是真正的代码 因为它被完全混淆了 这是图书馆正在做的事情 setup a ti
  • 帮助推文媒体实体

    我刚刚发现推文实体 我想将其添加到我的推文中 我已经读了一遍又一遍的API 但我仍然无法让它工作 这就是我所拥有的 entities array media url gt picture url url gt short url type
  • g++:如何整理导出的符号

    我正在尝试编译一个使用 JNI 的 Java 库 当我启动程序时 我看到崩溃并出现 UnsatisfiedLinkError 它表示在 DLL 中找不到特定方法 经过仔细检查 我发现我用于编译和链接的 g 通过在方法名称中添加 8 或 16
  • 分配的指针字段变为

    我有一个结构 type user struct Id string data ptr userData 我在全局范围内存储用户的一部分 type Hall struct users user var hall Hall global 最后
  • 使用 AVX2 向量化随机初始化并使用十进制数字数组打印 BigInt?

    如何将我的代码传递给 AVX2 代码并获得与以前相同的结果 是否可以使用 m256i在 LongNumInit 中 LongNumPrint 函数代替uint8 t L 或某种类似类型的变量 我对 AVX 的了解相当有限 我调查了很多 但是