我分析了一个程序,发现最耗时的部分是递归调用的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::string
改成了 std::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: 现在它运行得很慢。
发生的情况是,我的性能丢失了很多。相比于以前的6秒,我的示例程序现在需要44秒才能运行。再次使用vtune进行分析显示,一个函数被反复调用:std::swap
(对于gcc用户来说,这在bits/move.h中),而这个函数又被std::swap_ranges
(bits/stl_algobase.h)调用。
我想std::min
是使用quicksort
实现的,这就解释了为什么会有交换操作,但我不明白为什么在这种情况下交换需要这么长时间。
EDIT: 编译器选项:我正在使用带有选项“-O2 -g -DNDEBUG”和一堆警告指示符的gcc。
vector
,你交换的是指向底层数组的指针;如果你交换array
,则会使用临时副本来复制数组。 - Daniel Fischermin
,因为通过顺序扫描查找值比任何可能的排序算法都要快! - danielschemmelstd::min
imal示例吗(好的,我会停止;) - Christian Raucol.swap(prevCol);
。你应该尝试让std::array<...> *pCol, *pPrevCol;
指向两个真实的数组,通过指针更改所有访问并交换指针...或者将外部循环每次迭代两个并在第二个循环中手动交换col
和prevCol
(你需要在两个半部分之间添加额外的测试和分支语句,并且return
语句将需要某种条件判断)。 - CB Bailey