【问题标题】:Changing a vector into an array makes my program slower将向量更改为数组会使我的程序变慢
【发布时间】:2013-05-09 21:22:56
【问题描述】:

我分析了我的一个程序,发现热点是levenshtein_distance,递归调用。我决定尝试优化它。

lvh_distance levenshtein_distance( const std::string & s1, const std::string & s2 )
{
    const size_t len1 = s1.size(), len2 = s2.size();
    std::vector<unsigned int> col( len2+1 ), prevCol( len2+1 );

    const size_t prevColSize = prevCol.size();
    for( unsigned int i = 0; i < prevColSize; i++ )
        prevCol[i] = i;

    for( unsigned int i = 0, j; i < len1; ++i )
    {
        col[0] = i+1;
        const char s1i = s1[i];
        for( j = 0; j < len2; ++j )
        {
            const auto minPrev = 1 + std::min( col[j], prevCol[1 + j] );
            col[j+1] = std::min( minPrev, prevCol[j] + ( static_cast<unsigned int>( s1i != s2[j] ) ) );
        }
        col.swap( prevCol );
    }
    return prevCol[len2];
}

TL;DR:我改变了std::stringstd::array

战争故事: 在它上面运行 vtune 之后,我发现更新 col[j+1] 的那一行会减慢一切(90% 的时间都花在它上面)。我想:好吧,也许这是一个别名问题,也许编译器无法确定字符串对象中的字符数组是无别名的,因为它们被字符串接口屏蔽,并且花费 90% 的时间检查程序的其他部分修改它们。

所以我把我的字符串改成了一个静态数组,因为那里没有动态内存,下一步就是使用restrict来帮助编译器。但与此同时,我决定检查一下我这样做是否获得了任何性能。

lvh_distance levenshtein_distance( const std::string & s1, const std::string & s2 )
{
    const size_t len1 = s1.size(), len2 = s2.size();
    static constexpr unsigned MAX_STRING_SIZE = 512;
    assert(len1 < MAX_STRING_SIZE && len2 < MAX_STRING_SIZE);
    static std::array<unsigned int, MAX_STRING_SIZE> col, prevCol;

    for( unsigned int i = 0; i < len2+1; ++i )
        prevCol[i] = i;

    // the rest is unchanged
}

TL;DR:现在它运行缓慢。

发生的事情是我失去了表现。很多。我的示例程序现在在 44 秒内运行,而不是在 ~ 6 秒内运行。再次使用 vtune 进行分析表明,一个函数被一遍又一遍地调用:std::swap(对你来说,gcc 人,这是在 bits/move.h 中),它又被 std::swap_ranges(bits/stl_algobase.h)调用。 h)。

我认为std::min 是使用quicksort 实现的,这就解释了为什么要进行交换,但我不明白为什么在这种情况下交换需要这么多时间。

编辑:编译器选项:我正在使用带有选项“-O2 -g -DNDEBUG”和一堆警告说明符的 gcc。

【问题讨论】:

  • 我猜如果你交换vectors,你交换指向底层数组的指针,如果你交换arrays,数组是使用临时复制的。
  • min 永远不应该通过排序来实现,因为通过顺序扫描查找值比任何可能的排序算法都快!
  • @dionadar 你的意思是std::minimal 例子(好吧,我会停下来;))
  • 在优化的构建中,线条很容易混淆,你确定这不是真正的瓶颈吗:col.swap( prevCol );。您应该尝试让std::array&lt;...&gt; *pCol, *pPrevCol; 指向两个真实数组,通过指针更改所有访问并交换指针......或者一次执行外部循环两次迭代并手动交换colprevCol后半部分(你需要一个额外的测试并在两半之间休息,return 语句需要某种条件)。
  • @CharlesBailey 使用 Howard Hinnant 的 stack allocator for std::vector 结合了两全其美:指针交换和无动态分配。

标签: c++ arrays optimization vector


【解决方案1】:

对于一个实验,我用一对短字符串运行了一个基本未修改的原始代码版本,数组版本的时间约为 36 秒,向量版本的时间约为 8 秒。

您的版本似乎很大程度上取决于MAX_STRING_SIZE 的选择。当我使用 50 而不是 512(正好适合我的琴弦)时,数组版本的时间下降到大约 16 秒。

然后,我对您的主循环进行了手动翻译,以摆脱显式交换。这进一步将数组版本的时间减少到了 11s,更有趣的是,现在使数组版本的时间与MAX_STRING_SIZE 的选择无关。放回512的时候,数组版本还是用了11s。

这很好地证明了数组的显式交换是您的版本的大部分性能问题所在。

数组和向量版本之间仍然存在显着差异,数组版本的通话时间要长约 40%。我还没有机会确切地调查为什么会这样。

for( unsigned int i = 0, j; i < len1; ++i )
{
    {
        col[0] = i+1;
        const char s1i = s1[i];
        for( j = 0; j < len2; ++j )
        {
            const auto minPrev = 1 + std::min( col[j], prevCol[1 + j] );
            col[j+1] = std::min( minPrev, prevCol[j] + ( static_cast<unsigned int>( s1i != s2[j] ) ) );
        }
    }

    if (!(++i < len1))
        return col[len2];

    {
        prevCol[0] = i+1;
        const char s1i = s1[i];
        for( j = 0; j < len2; ++j )
        {
            const auto minPrev = 1 + std::min( prevCol[j], col[1 + j] );
            prevCol[j+1] = std::min( minPrev, col[j] + ( static_cast<unsigned int>( s1i != s2[j] ) ) );
        }
    }
}
return prevCol[len2];

【讨论】:

    【解决方案2】:

    首先:@DanielFischer 很可能已经指出了导致性能下降的原因:交换 std::arrays 是线性时间操作,而交换 std::vector 是恒定时间操作。虽然一些编译器可能能够优化这一点,但您的 gcc 似乎无法这样做。

    同样重要的是:像您在此处所做的那样使用 static 数组会使您的代码本质上不是线程安全的。这通常不是一个好主意。

    删除其中一个数组(或向量)和相关的交换并使用动态分配的 c 数组实际上非常容易,并且可以带来卓越的性能(至少对于我的设置而言)。
    再进行一些转换(如始终使用size_t)会产生以下函数:

    unsigned int levenshtein_distance3( const std::string & s1, const std::string & s2 )
    {
        const size_t len1 = s1.size(), len2 = s2.size();
        ::std::unique_ptr<size_t[]> col(new size_t[len2 + 1]);
    
        for(size_t i = 0; i < len2+1; ++i )
            col[i] = i;
    
        for(size_t i = 0; i < len1; ++i )
        {
            size_t lastc = col[0];
            col[0] = i+1;
            const char s1i = s1[i];
            for(size_t j = 0; j < len2; ++j )
            {
                const auto minPrev = 1 + (::std::min)(col[j], col[j + 1]);
                const auto newc = (::std::min)(minPrev, lastc + (s1i != s2[j] ? 1 : 0));
                lastc = col[j+1];
                col[j + 1] = newc;
            }
        }
        return col[len2];
    }
    

    【讨论】:

    • std::unique_ptr&lt;size_t[]&gt; col(new size_t[len2 + 1]); - 这不只是std::vector吗?
    • “虽然有些编译器可能会优化掉它,但你的 gcc 似乎无法做到这一点。” - 这种线性数组交换可以通过哪种方式优化掉?
    • @ChristianRau unique_ptr 与向量非常相似 - 除了它的开销更少,我认为 OP 想要实现这一点。我不完全确定 如何 我的 icc 优化了交换(虽然我假设,它将一个数组中的写入合并到另一个数组中),但它肯定做到了 :)
    • 事实上,一开始我什至完全无法重现该问题:通过优化,OP 的数组版本甚至比矢量版本快 15%!
    • unique_ptr&lt;size_t[]&gt; 与向量非常相似 - 除了它的开销更少” - 那会是什么开销?这只是交换/移动额外的两个指针的开销std::vector 需要您需要,还是您认为std::vector 会带来更多开销?
    猜你喜欢
    • 1970-01-01
    • 2021-12-22
    • 1970-01-01
    • 2022-08-03
    • 1970-01-01
    • 2021-11-03
    • 2023-04-07
    • 2012-04-30
    • 1970-01-01
    相关资源
    最近更新 更多