C 谓词的高效并行字节计算;小于或等于;对于有符号整数
在各种情况下,如生物信息学,计算字节大小的整数就足够了。为了获得最佳性能,许多处理器体系结构提供SIMD指令集(如MMX、SSE、AVX),这些指令集将寄存器划分为字节、半字和字大小的组件,然后分别对相应的组件执行算术、逻辑和移位操作 但是,有些体系结构不提供此类SIMD指令,需要对其进行仿真,这通常需要大量的位旋转。目前,我正在研究SIMD比较,特别是有符号、字节大小的整数的并行比较。我有一个我认为使用可移植C代码非常有效的解决方案(请参阅下面的函数C 谓词的高效并行字节计算;小于或等于;对于有符号整数,c,algorithm,bit-manipulation,C,Algorithm,Bit Manipulation,在各种情况下,如生物信息学,计算字节大小的整数就足够了。为了获得最佳性能,许多处理器体系结构提供SIMD指令集(如MMX、SSE、AVX),这些指令集将寄存器划分为字节、半字和字大小的组件,然后分别对相应的组件执行算术、逻辑和移位操作 但是,有些体系结构不提供此类SIMD指令,需要对其进行仿真,这通常需要大量的位旋转。目前,我正在研究SIMD比较,特别是有符号、字节大小的整数的并行比较。我有一个我认为使用可移植C代码非常有效的解决方案(请参阅下面的函数vsetles4)。这是基于彼得·蒙哥马利(
vsetles4
)。这是基于彼得·蒙哥马利(Peter Montgomery)2000年在a中的一项观察,即(a+B)/2=(a和B)+(a XOR B)/2
在中间计算中没有溢出
这个特定的仿真代码(functionvsetles4
)是否可以进一步加速?对于第一个订单,任何基本操作计数较低的解决方案都符合要求。我正在寻找便携式ISO-C99解决方案,不使用特定于机器的内部函数。大多数体系结构都支持和n
(a和b),因此可以假设这在效率方面可以作为单个操作使用
#包括
/*
vsetles4将其输入视为字节数组,每个字节包含
[-128127]中的有符号整数。以字节方式计算,介于
“a”和“b”的对应字节,布尔谓词“小于”
或等于“,”作为[0,1]中的值输入结果的相应字节。
*/
/*参考实现*/
uint32\u t vsetles4\u ref(uint32\u t a,uint32\u t b)
{
uint8_t a0=(uint8_t)((a>>0)和0xff);
uint8_t a1=(uint8_t)((a>>8)和0xff);
uint8_t a2=(uint8_t)((a>>16)和0xff);
uint8_t a3=(uint8_t)((a>>24)和0xff);
uint8_t b0=(uint8_t)((b>>0)和0xff);
uint8_t b1=(uint8_t)((b>>8)和0xff);
uint8_t b2=(uint8_t)((b>>16)和0xff);
uint8_t b3=(uint8_t)((b>>24)和0xff);
int p0=(int32_t)(int8_t)a0为了[可能]节省一些工作并回答用户2357112的问题,我为此创建了一个[粗略的]基准。我一次添加一个字节作为基准引用:
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <time.h>
long opt_R;
long opt_N;
void *aptr;
void *bptr;
void *cptr;
/*
vsetles4 treats its inputs as arrays of bytes each of which comprises
a signed integers in [-128,127]. Compute in byte-wise fashion, between
corresponding bytes of 'a' and 'b', the boolean predicate "less than
or equal" as a value in [0,1] into the corresponding byte of the result.
*/
/* base implementation */
void
vsetles4_base(const void *va, const void *vb, long count, void *vc)
{
const char *aptr;
const char *bptr;
char *cptr;
long idx;
count *= 4;
aptr = va;
bptr = vb;
cptr = vc;
for (idx = 0; idx < count; ++idx)
cptr[idx] = (aptr[idx] <= bptr[idx]);
}
/* reference implementation */
static inline uint32_t
_vsetles4_ref(uint32_t a, uint32_t b)
{
uint8_t a0 = (uint8_t)((a >> 0) & 0xff);
uint8_t a1 = (uint8_t)((a >> 8) & 0xff);
uint8_t a2 = (uint8_t)((a >> 16) & 0xff);
uint8_t a3 = (uint8_t)((a >> 24) & 0xff);
uint8_t b0 = (uint8_t)((b >> 0) & 0xff);
uint8_t b1 = (uint8_t)((b >> 8) & 0xff);
uint8_t b2 = (uint8_t)((b >> 16) & 0xff);
uint8_t b3 = (uint8_t)((b >> 24) & 0xff);
int p0 = (int32_t)(int8_t)a0 <= (int32_t)(int8_t)b0;
int p1 = (int32_t)(int8_t)a1 <= (int32_t)(int8_t)b1;
int p2 = (int32_t)(int8_t)a2 <= (int32_t)(int8_t)b2;
int p3 = (int32_t)(int8_t)a3 <= (int32_t)(int8_t)b3;
return (((uint32_t)p3 << 24) | ((uint32_t)p2 << 16) |
((uint32_t)p1 << 8) | ((uint32_t)p0 << 0));
}
uint32_t
vsetles4_ref(uint32_t a, uint32_t b)
{
return _vsetles4_ref(a,b);
}
/* Optimized implementation:
a <= b; a - b <= 0; a + ~b + 1 <= 0; a + ~b < 0; (a + ~b)/2 < 0.
Compute avg(a,~b) without overflow, rounding towards -INF; then
lteq(a,b) = sign bit of result. In other words: compute 'lteq' as
(a & ~b) + arithmetic_right_shift (a ^ ~b, 1) giving the desired
predicate in the MSB of each byte.
*/
static inline uint32_t
_vsetles4(uint32_t a, uint32_t b)
{
uint32_t m, s, t, nb;
nb = ~b; // ~b
s = a & nb; // a & ~b
t = a ^ nb; // a ^ ~b
m = t & 0xfefefefe; // don't cross byte boundaries during shift
m = m >> 1; // logical portion of arithmetic right shift
s = s + m; // start (a & ~b) + arithmetic_right_shift (a ^ ~b, 1)
s = s ^ t; // complete arithmetic right shift and addition
s = s & 0x80808080; // MSB of each byte now contains predicate
t = s >> 7; // result is byte-wise predicate in [0,1]
return t;
}
uint32_t
vsetles4(uint32_t a, uint32_t b)
{
return _vsetles4(a,b);
}
/* Optimized implementation:
a <= b; a - b <= 0; a + ~b + 1 <= 0; a + ~b < 0; (a + ~b)/2 < 0.
Compute avg(a,~b) without overflow, rounding towards -INF; then
lteq(a,b) = sign bit of result. In other words: compute 'lteq' as
(a & ~b) + arithmetic_right_shift (a ^ ~b, 1) giving the desired
predicate in the MSB of each byte.
*/
static inline uint64_t
_vsetles8(uint64_t a, uint64_t b)
{
uint64_t m, s, t, nb;
nb = ~b; // ~b
s = a & nb; // a & ~b
t = a ^ nb; // a ^ ~b
m = t & 0xfefefefefefefefell; // don't cross byte boundaries during shift
m = m >> 1; // logical portion of arithmetic right shift
s = s + m; // start (a & ~b) + arithmetic_right_shift (a ^ ~b, 1)
s = s ^ t; // complete arithmetic right shift and addition
s = s & 0x8080808080808080ll; // MSB of each byte now contains predicate
t = s >> 7; // result is byte-wise predicate in [0,1]
return t;
}
uint32_t
vsetles8(uint64_t a, uint64_t b)
{
return _vsetles8(a,b);
}
void
aryref(const void *va,const void *vb,long count,void *vc)
{
long idx;
const uint32_t *aptr;
const uint32_t *bptr;
uint32_t *cptr;
aptr = va;
bptr = vb;
cptr = vc;
for (idx = 0; idx < count; ++idx)
cptr[idx] = _vsetles4_ref(aptr[idx],bptr[idx]);
}
void
arybest4(const void *va,const void *vb,long count,void *vc)
{
long idx;
const uint32_t *aptr;
const uint32_t *bptr;
uint32_t *cptr;
aptr = va;
bptr = vb;
cptr = vc;
for (idx = 0; idx < count; ++idx)
cptr[idx] = _vsetles4(aptr[idx],bptr[idx]);
}
void
arybest8(const void *va,const void *vb,long count,void *vc)
{
long idx;
const uint64_t *aptr;
const uint64_t *bptr;
uint64_t *cptr;
count >>= 1;
aptr = va;
bptr = vb;
cptr = vc;
for (idx = 0; idx < count; ++idx)
cptr[idx] = _vsetles8(aptr[idx],bptr[idx]);
}
double
tvgetf(void)
{
struct timespec ts;
double sec;
clock_gettime(CLOCK_REALTIME,&ts);
sec = ts.tv_nsec;
sec /= 1e9;
sec += ts.tv_sec;
return sec;
}
void
timeit(void (*fnc)(const void *,const void *,long,void *),const char *sym)
{
double tvbeg;
double tvend;
tvbeg = tvgetf();
fnc(aptr,bptr,opt_N,cptr);
tvend = tvgetf();
printf("timeit: %.9f %s\n",tvend - tvbeg,sym);
}
// fill -- fill array with random numbers
void
fill(void *vptr)
{
uint32_t *iptr = vptr;
for (long idx = 0; idx < opt_N; ++idx)
iptr[idx] = rand();
}
// main -- main program
int
main(int argc,char **argv)
{
char *cp;
--argc;
++argv;
for (; argc > 0; --argc, ++argv) {
cp = *argv;
if (*cp != '-')
break;
switch (cp[1]) {
case 'R':
opt_R = strtol(cp + 2,&cp,10);
break;
case 'N':
opt_N = strtol(cp + 2,&cp,10);
break;
default:
break;
}
}
if (opt_R == 0)
opt_R = 1;
srand(opt_R);
printf("R=%ld\n",opt_R);
if (opt_N == 0)
opt_N = 100000000;
printf("N=%ld\n",opt_N);
aptr = calloc(opt_N,sizeof(uint32_t));
bptr = calloc(opt_N,sizeof(uint32_t));
cptr = calloc(opt_N,sizeof(uint32_t));
fill(aptr);
fill(bptr);
timeit(vsetles4_base,"base");
timeit(aryref,"aryref");
timeit(arybest4,"arybest4");
timeit(arybest8,"arybest8");
timeit(vsetles4_base,"base");
return 0;
}
请注意,您的引用并没有比一次一个字节的速度快太多[IMO,几乎不值得这么复杂]
除了SIMD之外,您的优化算法确实提供了最佳性能,我将其扩展为使用uint64\u t
,这[自然]使速度再次翻倍
对您来说,对SIMD版本进行基准测试可能也很有趣。只是为了证明它们真的是最快的。您是否真的对其进行了计时,使其比通过一系列int8\t
s并应用更快?这是一个非常有趣的问题。@QPaysTaxes我在这里“展示我的工作”(以免有人认为我想把工作转嫁给SO用户),而不是查看我的代码(“你不应该使用一个字母的变量名”:-)。我以前解决过这个问题,几年前开始使用11或12条指令,然后在前几天回答其他问题时,我偶然发现了这个问题中改进的代码。@AldwinCheung我最初是为NVIDIA GPU研究这个问题的。开普勒体系结构(大部分)有这种操作在硬件方面,以下GPU架构不需要也不需要仿真。在NVIDIA期间,我创建了一组较大的此类原语(,BSD许可证)。我已经退休,但有一个问题促使我重新访问此仿真,它与架构无关,很有趣(对于低端ARM也可能有用)@QPaysTaxes据我所知,优化问题在这里不是离题的(我非常熟悉的网站),因此应该没有必要迁移到CodeReview.SE(我根本不熟悉的网站)。此外,该问题被标记为“不清楚”而不是“离题”,不清楚原因。我会保持现状,这是我爱好的一部分,我不迫切需要找到更好的解决方案,我只是好奇编写此代码的潜在优越方法。感谢您投入所有工作。我目前没有访问硬件平台的权限。硬件SIMD支持什么(一种特殊的GPU体系结构),但根据我对该体系结构的了解,该硬件的速度应该是这里我的vsetles4
仿真代码的两倍左右(SIMD指令的吞吐量是常规ALU指令的1/4,执行顺序正确)。我的原始代码(BSD许可证)可以找到。该界面是固定的,因为运输软件,并完全如图所示。
R=1
N=100000000
timeit: 0.550527096 base
timeit: 0.483014107 aryref
timeit: 0.236460924 arybest4
timeit: 0.147254944 arybest8
timeit: 0.440311432 base