我正在尝试编写非常有效的汉明距离代码。灵感来自 Wojciech Muła 极其聪明的 SSE3 popcount implementation ,我编写了一个 AVX2 等效解决方案,这次使用 256 位寄存器。 我预计基于所涉及操作的双倍并行度至少有 30%-40% 的改进,但令我惊讶的是,AVX2 代码有点慢(大约 2%)!
有人能告诉我我没有获得预期性能提升的可能原因吗?
展开,两个 64 字节 block 的 SSE3 汉明距离:
INT32 SSE_PopCount(const UINT32* __restrict pA, const UINT32* __restrict pB) {
__m128i paccum = _mm_setzero_si128();
__m128i a = _mm_loadu_si128 (reinterpret_cast<const __m128i*>(pA));
__m128i b = _mm_loadu_si128 (reinterpret_cast<const __m128i*>(pB));
__m128i err = _mm_xor_si128 (a, b);
__m128i lo = _mm_and_si128 (err, low_mask);
__m128i hi = _mm_srli_epi16 (err, 4);
hi = _mm_and_si128 (hi, low_mask);
__m128i popcnt1 = _mm_shuffle_epi8(lookup, lo);
__m128i popcnt2 = _mm_shuffle_epi8(lookup, hi);
paccum = _mm_add_epi8(paccum, popcnt1);
paccum = _mm_add_epi8(paccum, popcnt2);
a = _mm_loadu_si128 (reinterpret_cast<const __m128i*>(pA + 4));
b = _mm_loadu_si128 (reinterpret_cast<const __m128i*>(pB + 4));
err = _mm_xor_si128 (a, b);
lo = _mm_and_si128 (err, low_mask);
hi = _mm_srli_epi16 (err, 4);
hi = _mm_and_si128 (hi, low_mask);
popcnt1 = _mm_shuffle_epi8(lookup, lo);
popcnt2 = _mm_shuffle_epi8(lookup, hi);
paccum = _mm_add_epi8(paccum, popcnt1);
paccum = _mm_add_epi8(paccum, popcnt2);
a = _mm_loadu_si128 (reinterpret_cast<const __m128i*>(pA + 8));
b = _mm_loadu_si128 (reinterpret_cast<const __m128i*>(pB + 8));
err = _mm_xor_si128 (a, b);
lo = _mm_and_si128 (err, low_mask);
hi = _mm_srli_epi16 (err, 4);
hi = _mm_and_si128 (hi, low_mask);
popcnt1 = _mm_shuffle_epi8(lookup, lo);
popcnt2 = _mm_shuffle_epi8(lookup, hi);
paccum = _mm_add_epi8(paccum, popcnt1);
paccum = _mm_add_epi8(paccum, popcnt2);
a = _mm_loadu_si128 (reinterpret_cast<const __m128i*>(pA + 12));
b = _mm_loadu_si128 (reinterpret_cast<const __m128i*>(pB + 12));
err = _mm_xor_si128 (a, b);
lo = _mm_and_si128 (err, low_mask);
hi = _mm_srli_epi16 (err, 4);
hi = _mm_and_si128 (hi, low_mask);
popcnt1 = _mm_shuffle_epi8(lookup, lo);
popcnt2 = _mm_shuffle_epi8(lookup, hi);
paccum = _mm_add_epi8(paccum, popcnt1);
paccum = _mm_add_epi8(paccum, popcnt2);
paccum = _mm_sad_epu8(paccum, _mm_setzero_si128());
UINT64 result = paccum.m128i_u64[0] + paccum.m128i_u64[1];
return (INT32)result;
}
使用 AVX 的 256 位寄存器的未展开等效版本:
INT32 AVX_PopCount(const UINT32* __restrict pA, const UINT32* __restrict pB) {
__m256i paccum = _mm256_setzero_si256();
__m256i a = _mm256_loadu_si256 (reinterpret_cast<const __m256i*>(pA));
__m256i b = _mm256_loadu_si256 (reinterpret_cast<const __m256i*>(pB));
__m256i err = _mm256_xor_si256 (a, b);
__m256i lo = _mm256_and_si256 (err, low_mask256);
__m256i hi = _mm256_srli_epi16 (err, 4);
hi = _mm256_and_si256 (hi, low_mask256);
__m256i popcnt1 = _mm256_shuffle_epi8(lookup256, lo);
__m256i popcnt2 = _mm256_shuffle_epi8(lookup256, hi);
paccum = _mm256_add_epi8(paccum, popcnt1);
paccum = _mm256_add_epi8(paccum, popcnt2);
a = _mm256_loadu_si256 (reinterpret_cast<const __m256i*>(pA + 8));
b = _mm256_loadu_si256 (reinterpret_cast<const __m256i*>(pB + 8));
err = _mm256_xor_si256 (a, b);
lo = _mm256_and_si256 (err, low_mask256);
hi = _mm256_srli_epi16 (err, 4);
hi = _mm256_and_si256 (hi, low_mask256);
popcnt1 = _mm256_shuffle_epi8(lookup256, lo);
popcnt2 = _mm256_shuffle_epi8(lookup256, hi);
paccum = _mm256_add_epi8(paccum, popcnt1);
paccum = _mm256_add_epi8(paccum, popcnt2);
paccum = _mm256_sad_epu8(paccum, _mm256_setzero_si256());
UINT64 result = paccum.m256i_i64[0] + paccum.m256i_u64[1] + paccum.m256i_i64[2] + paccum.m256i_i64[3];
return (INT32)result;
}
我已经验证了编译器发出的输出汇编代码,它看起来不错,预期将内在指令直接转换为机器指令。我唯一注意到的是,在 AVX2 版本上,累积 4 个四字的人口计数的最后一行,它生成的代码比 SSE3 版本更复杂(只需累积 2 个四字即可获得人口数量),但我仍然希望吞吐量更快。
为四字累加生成的 AVX2 代码
vextractf128 xmm0, ymm2, 1
psrldq xmm0, 8
movd ecx, xmm2
movd eax, xmm0
vextractf128 xmm0, ymm2, 1
psrldq xmm2, 8
add eax, ecx
movd ecx, xmm0
add eax, ecx
movd ecx, xmm2
add eax, ecx
为四字累加生成的 SSE3 代码
movd ecx, xmm2
psrldq xmm2, 8
movd eax, xmm2
add eax, ecx
我的测试程序调用每个例程 100 万次,使用不同的输入值,但重复使用两个静态缓冲区来保存 pA 和 pB 参数的数据。在我对 CPU 架构的有限理解中,这个位置(一遍又一遍地重用相同的内存缓冲区)应该很好地预热 CPU 缓存,而不是受内存带宽问题的束缚,但除了可能的内存带宽之外,我不明白为什么没有性能提升。
测试例程
int _tmain(int argc, _TCHAR* argv[]) {
lookup = _mm_setr_epi8(
/* 0 */ 0, /* 1 */ 1, /* 2 */ 1, /* 3 */ 2,
/* 4 */ 1, /* 5 */ 2, /* 6 */ 2, /* 7 */ 3,
/* 8 */ 1, /* 9 */ 2, /* a */ 2, /* b */ 3,
/* c */ 2, /* d */ 3, /* e */ 3, /* f */ 4
);
low_mask = _mm_set1_epi8(0xf);
lookup256 = _mm256_setr_epi8(
/* 0 */ 0, /* 1 */ 1, /* 2 */ 1, /* 3 */ 2,
/* 4 */ 1, /* 5 */ 2, /* 6 */ 2, /* 7 */ 3,
/* 8 */ 1, /* 9 */ 2, /* a */ 2, /* b */ 3,
/* c */ 2, /* d */ 3, /* e */ 3, /* f */ 4,
/* 0 */ 0, /* 1 */ 1, /* 2 */ 1, /* 3 */ 2,
/* 4 */ 1, /* 5 */ 2, /* 6 */ 2, /* 7 */ 3,
/* 8 */ 1, /* 9 */ 2, /* a */ 2, /* b */ 3,
/* c */ 2, /* d */ 3, /* e */ 3, /* f */ 4
);
low_mask256 = _mm256_set1_epi8(0xf);
std::default_random_engine generator;
generator.seed(37);
std::uniform_int_distribution<UINT32> distribution(0, ULONG_MAX);
auto dice = std::bind( distribution, generator);
UINT32 a[16];
UINT32 b[16];
int count;
count = 0;
{
cout << "AVX PopCount\r\n";
boost::timer::auto_cpu_timer t;
for( int i = 0; i < 1000000; i++ ) {
for( int j = 0; j < 16; j++ ) {
a[j] = dice();
b[j] = dice();
}
count+= AVX_PopCount(a, b);
}
}
cout << count << "\r\n";
std::default_random_engine generator2;
generator2.seed(37);
std::uniform_int_distribution<UINT32> distribution2(0, ULONG_MAX);
auto dice2 = std::bind( distribution2, generator2);
count = 0;
{
cout << "SSE PopCount\r\n";
boost::timer::auto_cpu_timer t;
for( int i = 0; i < 1000000; i++ ) {
for( int j = 0; j < 16; j++ ) {
a[j] = dice2();
b[j] = dice2();
}
count+= SSE_PopCount(a, b);
}
}
cout << count << "\r\n";
getch();
return 0;
}
测试机是 Intel Corei7 4790,我使用的是 Visual Studio 2012 Pro。
最佳答案
除了注释中的小问题(为 /arch:AVX 编译)之外,您的主要问题是在每次迭代时生成随机输入数组。这是您的瓶颈,因此您的测试无法有效地评估您的方法。注意 - 我没有使用 boost,但 GetTickCount 可用于此目的。只考虑:
int count;
count = 0;
{
cout << "AVX PopCount\r\n";
unsigned int Tick = GetTickCount();
for (int i = 0; i < 1000000; i++) {
for (int j = 0; j < 16; j++) {
a[j] = dice();
b[j] = dice();
}
count += AVX_PopCount(a, b);
}
Tick = GetTickCount() - Tick;
cout << Tick << "\r\n";
}
产生输出:
AVX PopCount
2309
256002470
所以需要 2309 毫秒才能完成...但是如果我们完全摆脱您的 AVX 例程会发生什么?只需制作输入数组:
int count;
count = 0;
{
cout << "Just making arrays...\r\n";
unsigned int Tick = GetTickCount();
for (int i = 0; i < 1000000; i++) {
for (int j = 0; j < 16; j++) {
a[j] = dice();
b[j] = dice();
}
}
Tick = GetTickCount() - Tick;
cout << Tick << "\r\n";
}
产生输出:
Just making arrays...
2246
怎么样。这并不奇怪,真的,因为您要生成 32 个随机数,这可能非常昂贵,然后只执行一些相当快速的整数数学运算和洗牌。
所以...
现在让我们再增加 100 次迭代,让随机生成器脱离紧密循环。在此处禁用优化的编译将按预期运行您的代码,并且不会丢弃“无用”的迭代 - 大概我们在这里关心的代码已经(手动)优化!
for (int j = 0; j < 16; j++) {
a[j] = dice();
b[j] = dice();
}
int count;
count = 0;
{
cout << "AVX PopCount\r\n";
unsigned int Tick = GetTickCount();
for (int i = 0; i < 100000000; i++) {
count += AVX_PopCount(a, b);
}
Tick = GetTickCount() - Tick;
cout << Tick << "\r\n";
}
cout << count << "\r\n";
count = 0;
{
cout << "SSE PopCount\r\n";
unsigned int Tick = GetTickCount();
for (int i = 0; i < 100000000; i++) {
count += SSE_PopCount(a, b);
}
Tick = GetTickCount() - Tick;
cout << Tick << "\r\n";
}
cout << count << "\r\n";
产生输出:
AVX PopCount
3744
730196224
SSE PopCount
5616
730196224
那么恭喜 - 你可以拍拍自己的后背,你的 AVX 例程确实比 SSE 例程快了大约三分之一(这里在 Haswell i7 上测试)。教训是要确保您实际上正在分析您认为自己正在分析的内容!
关于c++ - AVX 256 位代码的性能略逊于等效的 128 位 SSSE3 代码,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/31466848/
我的目标是转换表单输入,例如“100兆字节”或“1GB”,并将其转换为我可以存储在数据库中的文件大小(以千字节为单位)。目前,我有这个:defquota_convert@regex=/([0-9]+)(.*)s/@sizes=%w{kilobytemegabytegigabyte}m=self.quota.match(@regex)if@sizes.include?m[2]eval("self.quota=#{m[1]}.#{m[2]}")endend这有效,但前提是输入是倍数(“gigabytes”,而不是“gigabyte”)并且由于使用了eval看起来疯狂不安全。所以,功能正常,
如何在buildr项目中使用Ruby?我在很多不同的项目中使用过Ruby、JRuby、Java和Clojure。我目前正在使用我的标准Ruby开发一个模拟应用程序,我想尝试使用Clojure后端(我确实喜欢功能代码)以及JRubygui和测试套件。我还可以看到在未来的不同项目中使用Scala作为后端。我想我要为我的项目尝试一下buildr(http://buildr.apache.org/),但我注意到buildr似乎没有设置为在项目中使用JRuby代码本身!这看起来有点傻,因为该工具旨在统一通用的JVM语言并且是在ruby中构建的。除了将输出的jar包含在一个独特的、仅限ruby
在rails源中:https://github.com/rails/rails/blob/master/activesupport/lib/active_support/lazy_load_hooks.rb可以看到以下内容@load_hooks=Hash.new{|h,k|h[k]=[]}在IRB中,它只是初始化一个空哈希。和做有什么区别@load_hooks=Hash.new 最佳答案 查看rubydocumentationforHashnew→new_hashclicktotogglesourcenew(obj)→new_has
我的瘦服务器配置了nginx,我的ROR应用程序正在它们上运行。在我发布代码更新时运行thinrestart会给我的应用程序带来一些停机时间。我试图弄清楚如何优雅地重启正在运行的Thin实例,但找不到好的解决方案。有没有人能做到这一点? 最佳答案 #Restartjustthethinserverdescribedbythatconfigsudothin-C/etc/thin/mysite.ymlrestartNginx将继续运行并代理请求。如果您将Nginx设置为使用多个上游服务器,例如server{listen80;server
我的主要目标是能够完全理解我正在使用的库/gem。我尝试在Github上从头到尾阅读源代码,但这真的很难。我认为更有趣、更温和的踏脚石就是在使用时阅读每个库/gem方法的源代码。例如,我想知道RubyonRails中的redirect_to方法是如何工作的:如何查找redirect_to方法的源代码?我知道在pry中我可以执行类似show-methodmethod的操作,但我如何才能对Rails框架中的方法执行此操作?您对我如何更好地理解Gem及其API有什么建议吗?仅仅阅读源代码似乎真的很难,尤其是对于框架。谢谢! 最佳答案 Ru
我的假设是moduleAmoduleBendend和moduleA::Bend是一样的。我能够从thisblog找到解决方案,thisSOthread和andthisSOthread.为什么以及什么时候应该更喜欢紧凑语法A::B而不是另一个,因为它显然有一个缺点?我有一种直觉,它可能与性能有关,因为在更多命名空间中查找常量需要更多计算。但是我无法通过对普通类进行基准测试来验证这一点。 最佳答案 这两种写作方法经常被混淆。首先要说的是,据我所知,没有可衡量的性能差异。(在下面的书面示例中不断查找)最明显的区别,可能也是最著名的,是你的
几个月前,我读了一篇关于rubygem的博客文章,它可以通过阅读代码本身来确定编程语言。对于我的生活,我不记得博客或gem的名称。谷歌搜索“ruby编程语言猜测”及其变体也无济于事。有人碰巧知道相关gem的名称吗? 最佳答案 是这个吗:http://github.com/chrislo/sourceclassifier/tree/master 关于ruby-寻找通过阅读代码确定编程语言的rubygem?,我们在StackOverflow上找到一个类似的问题:
我目前正在使用以下方法获取页面的源代码:Net::HTTP.get(URI.parse(page.url))我还想获取HTTP状态,而无需发出第二个请求。有没有办法用另一种方法做到这一点?我一直在查看文档,但似乎找不到我要找的东西。 最佳答案 在我看来,除非您需要一些真正的低级访问或控制,否则最好使用Ruby的内置Open::URI模块:require'open-uri'io=open('http://www.example.org/')#=>#body=io.read[0,50]#=>"["200","OK"]io.base_ur
前言作为一名程序员,自己的本质工作就是做程序开发,那么程序开发的时候最直接的体现就是代码,检验一个程序员技术水平的一个核心环节就是开发时候的代码能力。众所周知,程序开发的水平提升是一个循序渐进的过程,每一位程序员都是从“菜鸟”变成“大神”的,所以程序员在程序开发过程中的代码能力也是根据平时开发中的业务实践来积累和提升的。提高代码能力核心要素程序员要想提高自身代码能力,尤其是新晋程序员的代码能力有很大的提升空间的时候,需要针对性的去提高自己的代码能力。提高代码能力其实有几个比较关键的点,只要把握住这些方面,就能很好的、快速的提高自己的一部分代码能力。1、多去阅读开源项目,如有机会可以亲自参与开源
如何将send与+=一起使用?a=20;a.send"+=",10undefinedmethod`+='for20:Fixnuma=20;a+=10=>30 最佳答案 恐怕你不能。+=不是方法,而是语法糖。参见http://www.ruby-doc.org/docs/ProgrammingRuby/html/tut_expressions.html它说Incommonwithmanyotherlanguages,Rubyhasasyntacticshortcut:a=a+2maybewrittenasa+=2.你能做的最好的事情是: