【问题标题】:Fastest way to determine if two strings differ by a single character确定两个字符串是否相差一个字符的最快方法
【发布时间】:2016-05-02 11:58:26
【问题描述】:

我正在编写一个 C++ 算法,它接受两个字符串,如果您可以通过将单个字符更改为另一个字符来从字符串 a 变为字符串 b,则返回 true。 两个字符串的大小必须相等,并且只能有一个差异。 我还需要访问已更改的索引以及已更改的 strA 字符。 我找到了一个可行的算法,但它会遍历每一对单词,并且在任何大量输入时运行速度都太慢了。

bool canChange(std::string const& strA, std::string const& strB, char& letter)
{
    int dif = 0;
    int position = 0;
    int currentSize = (int)strA.size();
    if(currentSize != (int)strB.size())
    {
        return false;
    }
    for(int i = 0; i < currentSize; ++i)
    {
        if(strA[i] != strB[i])
        {
            dif++;
            position = i;
            if(dif > 1)
            {
                return false;
            }
        }
    }
    if(dif == 1)
    {
        letter = strA[position];
        return true;
    }
    else return false;
}

关于优化的任何建议?

【问题讨论】:

  • 您有什么样的优化方案?时间?记忆?顺便说一句,您还没有解决已更改字符的索引,您只是返回字符本身
  • 我可以看到canChange()(那个名字很有趣)检查字符对 - 你能详细说明every single pair of words吗?
  • 我认为“遍历每一对单词”是缓慢的根源;发布的代码看起来非常有效。如果您将文本 1 中的每个单词与文本 2 中的每个单词进行比较,这是一个 O(N^2) 操作,无论您优化 canChange() 多少,它都会很慢。
  • 太慢了有多慢?你使用了哪些编译选项,你的微基准是什么样的?你在什么硬件上运行它?英特尔哈斯韦尔?英特尔酷睿2?微基准测试对于现代 CPU 来说是困难,这要归功于缓存、分支预测和不知道何时优化掉重复相同工作的循环的编译器。在没有优化启用的情况下编译是不是一个选项,尤其是。不适用于需要大量内联包装函数才能生成可接受的 asm 的普通 C++。
  • 很遗憾,您的代码不能使用 gcc 或 clang 自动矢量化。它编译成一个相当紧凑的循环,每次迭代检查一个字节。 goo.gl/AAEA3A(从.L4jg .L4 的循环)。它在通常采用的循环内有一个条件分支。

标签: c++ string algorithm optimization char


【解决方案1】:

检查字符串中的所有字符有点困难,除非你能接受偶尔出现的错误结果。

我建议使用标准库的功能,而不是试图计算不匹配的数量。例如;

#include <string>
#include <algorithm>

bool canChange(std::string const& strA, std::string const& strB, char& letter, std::size_t &index)
{
     bool single_mismatch = false;
     if (strA.size() == strB.size())
     {
         typedef std::string::const_iterator ci; 
         typedef std::pair<ci, ci> mismatch_result;

         ci begA(strA.begin()), endA(strA.end());

         mismatch_result result = std::mismatch(begA, endA, strB.begin());

         if (result.first != endA)    //  found a mismatch
         {
             letter = *(result.first);
             index = std::distance(begA, result.first);

             // now look for a second mismatch

             std::advance(result.first, 1);
             std::advance(result.second, 1);

             single_mismatch = (std::mismatch(result.first, endA, result.second).first == endA);
         }
    }
    return single_mismatch;
}

这适用于所有版本。在 C++11 中可以稍微简化一下。

如果以上返回true,则发现单个不匹配。

如果返回值为false,则要么字符串大小不同,要么不匹配的数量不等于1(要么字符串相等,要么有多个不匹配)。

如果字符串长度不同或完全相等,letterindex 不变,否则会标识第一个不匹配项(strAindex 中的字符值)。

【讨论】:

    【解决方案2】:

    如果您想针对大部分相同的字符串进行优化,您可以使用 x86 SSE/AVX 向量指令。您的基本想法看起来不错:一旦检测到第二个差异就中断。

    要查找和计算字符差异,像 PCMPEQB / PMOVMSKB / test-and-branch 这样的序列可能很好。 (使用 C/C++ 内部函数来获取这些向量指令)。当您的向量循环检测到当前块中的非零差异时,POPCNT 位掩码以查看您是否刚刚找到第一个差异,或者您是否在同一块中找到了两个差异。

    我拼凑了我所描述的未经测试且未完全完善的 AVX2 版本。 此代码假定字符串长度是 32 的倍数。提前停止并使用清理尾声处理最后一个块作为练习留给读者。

    #include <immintrin.h>
    #include <string>
    
    // not tested, and doesn't avoid reading past the end of the string.
    // TODO: epilogue to handle the last up-to-31 left-over bytes separately.
    bool canChange_avx2_bmi(std::string const& strA, std::string const& strB, char& letter) {
        size_t size = strA.size();
        if (size != strB.size())
            return false;
    
        int diffs = 0;
        size_t diffpos = 0;
        size_t pos = 0;
        do {
            uint32_t diffmask = 0;
            while( pos < size ) {
                __m256i vecA  = _mm256_loadu_si256(reinterpret_cast<const __m256i*>(& strA[pos]));
                __m256i vecB  = _mm256_loadu_si256(reinterpret_cast<const __m256i*>(& strB[pos]));
                __m256i vdiff = _mm256_cmpeq_epi8(vecA, vecB);
                diffmask = _mm256_movemask_epi8(vdiff);
                pos += 32;
                if (diffmask) break;  // gcc makes worse code if you include && !diffmask in the while condition, instead of this break
            }
            if (diffmask) {
                diffpos = pos + _tzcnt_u32(diffmask);  // position of the lowest set bit.  Could safely use BSF rather than TZCNT here, since we only run when diffmask is non-zero.
                diffs += _mm_popcnt_u32(diffmask);
            }
        } while(pos < size && diffs <= 1);
    
        if (diffs == 1) {
            letter = strA[diffpos];
            return true;
        }
        return false;
    }
    

    丑陋的break 而不是在while 条件中包含它显然有助于gcc generate better codedo{}while() 也符合我希望 asm 出来的方式。我没有尝试使用 forwhile 循环来查看 gcc 会做什么。

    这样内循环真的很紧:

    .L14:
            cmp     rcx, r8
            jnb     .L10      #  the while(pos<size) condition
    .L6: # entry point for first iteration, because gcc duplicates the pos<size test ahead of the loop
    
            vmovdqu ymm0, YMMWORD PTR [r9+rcx]        # tmp118,* pos
            vpcmpeqb        ymm0, ymm0, YMMWORD PTR [r10+rcx]       # tmp123, tmp118,* pos
            add     rcx, 32   # pos,
            vpmovmskb       eax, ymm0     # tmp121, tmp123
            test    eax, eax        # tmp121
            je      .L14        #,
    

    理论上,这应该以每 2 个时钟一次迭代运行(Intel Haswell)。循环中有 7 个融合域微指令。 (应该是 6,但 2-reg addressing modes apparently can't micro-fuse on SnB-family CPUs。)由于其中两个 uops 是负载,而不是 ALU,因此在 SnB/IvB 上也可以实现此吞吐量。

    这对于飞越两个字符串相同的区域应该非常有用。如果字符串很短,和/或早期有多个差异,正确处理任意字符串长度的开销可能会比简单的标量函数慢。

    【讨论】:

    • 代替 popcnt,您可以使用奇偶校验来查看 popcnt 是奇数还是偶数。但在 x86(可用 AVX2)上查找 32 位整数奇偶校验的最快方法是使用 popcnt(x) &amp; 1。不需要 SSE4 的 SSE 版本可能想要这样做。 (x86 可以使用 xor al, ah / setp al 进行 16 位奇偶校验(PF 设置为偶数奇偶校验,IIRC)。
    【解决方案3】:

    您的输入有多大?

    我认为 strA[i], strB[i] 具有函数调用开销,除非它是内联的。因此,请确保在打开内联并在发布时编译的情况下进行性能测试。否则,请尝试使用 strA.c_str() 将字节作为 char* 获取。

    如果一切都失败了,但速度仍然不够快,请尝试将您的字符串分成块并在这些块上使用 memcmp 或 strncmp。如果没有差异,则移动到下一个块,直到到达终点或找到​​差异。如果发现差异,请逐字节比较,直到找到差异。我建议这条路线,因为 memcmp 通常比您的琐碎实现更快,因为它们可以利用处理器 SSE 扩展等来进行非常快速的比较。

    另外,您的代码也有问题。您假设 strA 比 strB 长,并且只检查数组访问器的 A 长度。

    【讨论】:

    • ... only checking the length of A for the array accessors - 除非两个字符串长度相同,否则函数会提前终止。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2023-02-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-09-06
    • 2011-04-25
    • 2015-05-18
    相关资源
    最近更新 更多