Agner Fog's VCL
,如果您的项目许可证与GPL兼容)。还有一些非GPLed包装。
,如果您可以使用Eigen的现有模板,但使用另一个使用位的类,那将非常好
and
在整数上,而不是在浮点上乘法。我不确定这是否可行。
我做了一些搜索,大多数关于二进制矩阵的文献都是关于生成布尔结果的(包括一些问题)
like this
一般来说,向量整数并不是很慢,而是
只是
向量整数乘法速度很慢,尤其是与向量相比-
float
最新x86硬件上的FMA(特别是Intel,其在Haswell和更高版本上的浮点FMA吞吐量为每个时钟2x 256b向量)。
当然,这假设一个整数matmul实现使用与等效FP matmul相同的缓存阻塞和其他优化,如果您不想(或不知道如何)自己编写,并且找不到一个库来为您编写,那么问题就出在这里。
标题问题的答案是非常明确的
,使用实际乘法非常浪费时间,尤其是使用32位元素。
存储格式选项:
-
密度的4倍
(每个向量的缓存占用空间/内存带宽/元素)
-
-
在重要的情况下,垂直加法很容易(例如,在外循环上进行矢量化,并同时处理多行或多列。如果您以一种不需要额外洗牌的方式交错数据,则可以很好(避免最后的水平和)
每字节4个元素,压缩到低半字节中
:
-
-
使用AVX2进行popcount非常有效
vpshufb
vpand
). 当L2带宽受限时(每个周期约32B负载),每个时钟运行64个元素。见下文。
-
-
压缩位
:
每字节6个元素,
0xxx0xxx
(这个问题在HSW/SKL上可能没有优势,但值得考虑):
-
6x独立字节的密度
-
-
使用AVX2优化了有效的popcount
vpshufb
2倍前无需遮罩
,只需右移一次。(
如果设置了高位,则将字节归零,否则将使用低位半字节作为索引。这就是为什么它需要屏蔽。)将此格式右移4(
vpsrld ymm0,4
)仍将在每个字节的高位保留零。加载+和->累积popcount是每个向量7个融合域UOP(
vmovdqa
vpand ymm,[mem]
/
vpsrld ymm,4
vpshufb
/2倍
vpaddb
-
这个
-
如果我们需要的话可能会有用
popcount(A[])
popcount(A[] & B[])
或者对于不同的微体系结构,其中ALU与负载吞吐量不同。
另一个变体,
使用单个AVX512VBMI(Cannonlake?)可以统计每个字节7个元素
vpermi2b
(
_mm512_permutex2var_epi8
)
vpshufb
使用AVX512VBMI计数压缩-8(但不包括
AVX512VPOPCNTDQ
vpermi2b
要计算低7,请按住shift键并屏蔽顶部位,然后将其相加。(单个位的popcount=该位)。
uint8_t
元素更容易有效地洗牌(因为有字节洗牌,如
vpshufb
),因此,如果您必须在飞行中进行转置,则可能值得考虑。或者在转换时只在飞行中打包到位?
这对于缓存占用空间/内存带宽来说也是一件大事
.8大小差异的因素可能意味着整行或整列只占用一级缓存的一部分,而不是溢出一级缓存。因此,它可以使缓存阻塞更容易/不那么重要。
1MiB on Skylake-AVX512
),但将适用于许多核心Xeon CPU上的L3。但是,L3由所有核心(包括云环境中的其他虚拟机)竞争性共享,并且比L2慢得多。(像您将在HPC/cloud系统中运行的许多核心Xeon的单核内存带宽低于四核桌面,因为三级缓存的延迟更高,并发性没有增加(请参阅
"latency-bound platforms" section of this answer
。在Xeon上驱动相同数量的内存带宽需要更多的内核,即使总吞吐量更高。但是如果你能让每个核心都在其私有L2上工作,你会收获很多。)
:您已经安排了循环,因此需要将一次布尔运算减少到非零计数。这是一件好事。
使用8位整数0/1元素,最多可以执行255个操作
vpaddb
vpsadbw
against an all-zero vector to horizontally add the bytes in a vector into 64-bit integers
.然后将蓄能器与
vpaddq
,
then horizontally sum it
VPSHUFB
-基于位切片的popcount。(参见
http://wm.ite.pl/articles/sse-popcount.html
例如如果必须手动对其进行矢量化,则需要使用内部函数而不是asm来编写。)
您可以考虑将数据打包为每字节4位的低半字节。
vpshufb
可以计算AND结果的每个字节中的位,而不需要任何移位/掩蔽。在内环中,有两个负载,
,
vpaddb
。通过适当的展开,这应该与每个时钟2x 32B的L1D负载带宽保持一致,并使所有三个矢量执行端口(在Haswell或Skylake上)饱和。每隔128个或255个向量或其他什么来累积累加器的字节
vpsadbw
因此,最内部的循环应该以每字节4个元素*每向量32B=每时钟周期128个元素的速度运行,
如果您可以安排它读取L1D缓存中的热数据。预计Haswell/Skylake上的二级缓存的带宽将达到一半左右,而三级缓存的带宽则要差得多。
具有
如果元素为0或1,则可以使用一些整数乘加指令。它们的设计有点奇怪,用于不同的用例,而不是FP FMA。它们将乘法结果的水平对相加,生成更宽的元素。
VPMADDUBSW
vpsadbw
vpsadbw
,这对你毫无益处
vpand
。只有当您想使用
vpaddw
vpmaddubsw doesn't seem useful here, because
使用SSE/AVX可以有效地将0/1整数转换为位图
:对于32位整数元素,
vpslld ymm0, 31
将相关位左移到每个元素的顶部,然后
vmovmskps eax, ymm0
获取每个32位元素的高字节的8位掩码。对于8位整数元素,
vpslld ymm0, 7
/
vpmovmskb eax, ymm0
执行相同的操作,但每个字节都会生成一个32位整数位图结果。(只有每个字节的符号位很重要,因此没有只有8位粒度的移位指令是很好的。您不需要对进入下一个元素的位做任何事情。)
这不是一种很好的立即使用向量的方法,因为最终结果会出现在整数寄存器中。这不是一种很好的即时生成和使用的格式,但它是最紧凑的,因此如果您可以长期保持这种格式的矩阵,那么它是有意义的。(如果加载时内存带宽有限。)
:单向是2x
vpackssdw
vpacksswb
。因为它们在128b车道内运行,您的元素最终将被重新排序。但只要每一行/列的顺序相同,就可以了。只有当您想要获取一个不是以32个元素的倍数开始的行/列块时,这才是一个问题。这里的另一个选项是左移(8、16和24),和或向量一起。事实上
static inline
__m256i load_interleave4x32(const int32_t *input) {
const char *p = (const char*)input;
__m256i t0 = _mm256_load_si256((const __m256i*)(p));
__m256i t1 = _mm256_load_si256((const __m256i*)(p+32*1-1));
__m256i t2 = _mm256_load_si256((const __m256i*)(p+32*2-2));
__m256i t3 = _mm256_load_si256((const __m256i*)(p+32*3-3));
return t0 | t1 | t2 | t3;
}
转换为每字节4位半压缩
:我们可以使用与上述相同的想法。从中获取4个向量
load_interleave4x32
uint8\u t
set1_epi8(1)
如果以这种格式存储整个矩阵,则可以将其用作转置的一部分,或者可以使用这种格式存储缓存阻塞转置的临时副本。matmul会多次接触每一行/每一列,因此第一次制作紧凑格式可能值得做额外的工作,因为这样可以在后续过程中为每个向量做4倍的工作。
带AVX512BW(Skylake-AVX512)
popcnt
@Harold points out an interesting identity
这让我们能够以额外的整数运算为代价,进行2/3倍多的向量popcounts。
popcnt(a) + popcnt(b) + popcnt(c)
= popcnt(a ^ b ^ c) + 2 * popcnt((a ^ b) & c | (a & b))
a ^ b ^ c
(a ^ b) & c | (a & b)
可以用一个
vpternlogd
2*
如果我们使用单独的预移位
vpshufb
LUT向量。另请参见
this implementation that uses 30x
vpternlogd
+ 1 vector popcnt to handle 16 vectors of 512b
16*popcnt
循环内计数;其他一切都是连锁的)。
这对于计算完全压缩的每字节8位元素很可能是值得的,并且使该格式对AVX512更有吸引力,相比之下,为没有太多移位/掩蔽的popcounting优化的密度较低的格式。
VPERNLOGD
如果是字节粒度,则也可以用作转置的位混合指令
VPBLENDMB zmm{k1}, zmm, zmm
纹理不够精细。
对于某些CPU上的AVX2来说,这可能是值得的,也许可以避免每4或5个向量popcounts中就有1个,而不是3个向量popcounts中的1个?或者,如果它只是增加了执行端口的总压力,并且在任何特定端口上都没有瓶颈,那么它可能根本没有帮助。这对标量很有用
指令(可能在没有AVX2的CPU上),因为这些指令在Intel CPU的单个端口上会出现瓶颈。
uint8\u t
将布尔元素转换为非交错位图的效率略高于AVX2(甚至不需要移位),相反的转换效率更高。测试掩码或将掩码与set1\u epi8(1)的向量进行比较都可以完成这项工作,从64字节的输入中生成64位掩码。或者从32位整数开始,一次生成16位掩码。您可以有效地将这些位与
kunpck
_mm512_test_epi8_mask
(
vptestmb
)
有趣的是:将两个向量合并在一起,并生成真/假字节元素的掩码寄存器结果。但这并不是我们真正想要的:如果我们要打包我们的位,我们希望将其作为输入矩阵的预处理步骤,而不是在进行内积时即时进行。
位图->0/-1的向量很快:
__m512i _mm512_movm_epi8 (__mmask64 k)
(
vpmovm2b
)
-1
而不是添加
1
,但必须先屏蔽它,然后才能将一个字节中的多个位合并在一起。
,您没有512b
vpshufb
AVX512 popcnt extension
直接用于矢量popcnt,但尚未宣布其硬件。(AVX2
vpshufb ymm
但是,KNL速度非常慢:每12个周期一次,并且
psadbw ymm
a bithack popcnt based on 32-bit integer elements, since that's just AND/shift/ADD
.32位元素执行popcnt的步骤将少于64位,但仍然足够大,不会溢出合理的问题大小(因此可以将向量的水平和延迟到循环外)
考虑到存储格式的选择,对KNL来说,每字节打包多个位可能不是一个好主意,但单字节整数元素很好。
vpandd zmm
和
vpaddd zmm
SWAR
转置位:
BMI2
_pdep_u64
另一个有用的选择是
vpmovmskb
this blog post
How would you transpose a binary matrix?
.
在matmul中使用
有些选择取决于输入数据的格式,以及重复使用相同矩阵的频率。如果一个矩阵将被多次使用,那么提前将其压缩到每字节4或8位是有意义的。(或第一次使用时正在飞行中)。保留它的转置副本也可能有意义,尤其是当它始终是需要转置的乘法的一侧时。(如果您有时需要一种方式,有时需要另一种方式,那么动态重做可能会更好地占用三级缓存空间。但这些缓存空间足够大,您可能不会获得太多三级命中率,因此只保留一个转置副本就可以了。)
或者甚至可以在从输入格式转换时写出转置和非转置版本。
当缓存阻塞一个普通的FP matmul时,同样的原则也适用,所以请阅读这篇文章。
使用位集
&
因为整个列将把值放回内存中,然后在中再次循环
.count()
关于结果。我怀疑编译器是否会将其优化为使用
VPSHUFB
-基于每个矢量的位切片popcnt
VPAND
http://wm.ite.pl/articles/sse-popcount.html
根据矩阵大小,至少内部循环可能会命中L1D缓存,但两次循环产生的额外加载/存储指令开销更大,并且还会干扰有价值数据的预取。
这并不容易。唯一不糟糕的是
clang++ -stdlib=libc++
vector<bool>
std::count(v.begin(), v.end(), true);
到矢量化
vpshufb
+
+
vpaddb
在展开的环内
vpsadbw + vpaddq
每次迭代一次,但这对于自动矢量化的代码非常好。
向量(<);布尔>
也是位图,但是
std::count(v.begin(),v.end(),true);
非常糟糕:它使用了一个完全幼稚的循环,每次测试1位。它甚至不能有效地做到这一点。相同的
clang++
默认情况下
libstdc++
而不是新的
libc++
.
boost::dynamic_bitset
.count()
成员函数,但它没有利用
popcnt公司
指令或AVX2。它执行逐字节LUT查找。这比
std::count(vector<bool>)
没有libc++,但对于HPC来说,它甚至不够好。
这是测试代码
on the Godbolt compiler explorer
,具有gcc和clang asm输出。他们都用过
-march=haswell
.
但不幸的是,似乎没有一种有效的方法来实现按位和2
std::vector<bool>
This answer
展示了如何获得g++的底层实现
,但该代码不会自动矢量化。做同样的事情
并对其进行调整,使其自动矢量化
可以
向量(<);布尔>
,因为向量的向量是一个糟糕的额外间接级别。如果问题的转置部分也对性能至关重要,那么使用标准容器访问有效的popcount并不能解决整个问题。
对于
std::bitset<1024*1024>.count()
锂离子电池++
popcnt公司
说明,其中(根据
this
On
vector<bool>
â Howard Hinnant
,了解有关C++标准库的一些评论,以及为什么位数组是一种有用的数据结构,但是
向量(<);布尔>
bool
-每字节
bool[]
数组,与朴素的
(就像没有libc++的gcc和clang一样)。