std::string的性能优化

3
当我在我的应用程序中进行性能测试时,我注意到以下代码(Visual Studio 2010)有所不同。
较慢的版本
while(heavyloop)
{
   if(path+node+"/" == curNode)
   {
        do something
   }
}

这将导致生成结果字符串时需要额外的malloc操作。
为了避免这些malloc操作,我进行了以下更改:
std::string buffer;
buffer.reserve(500);   // Big enough to hold all combinations without the need of malloc

while(heavyloop)
{
   buffer = path;
   buffer += node;
   buffer += "/";

   if(buffer == curNode)
   {
        do something
   }
}

虽然第二个版本与第一个版本相比看起来有点尴尬,但它仍然足够可读。我想知道的是,这种优化是编译器疏忽导致的,还是必须手动完成。由于它只改变了分配的顺序,我希望编译器也可以自己解决。另一方面,某些条件必须满足才能真正实现优化,这些条件可能没有得到满足,但如果条件不满足,代码至少会像第一个版本一样执行。新版本的Visual Studio在这方面是否更好?
下面是一个SSCE演示区别:
std::string gen_random(std::string &oString, const int len)
{
    static const char alphanum[] =
        "0123456789"
        "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
        "abcdefghijklmnopqrstuvwxyz";

    oString = "";

    for (int i = 0; i < len; ++i)
    {
        oString += alphanum[rand() % (sizeof(alphanum) - 1)];
    }

    return oString;
}

int main(int argc, char *argv[])
{
    clock_t start = clock();
    std::string s = "/";
    size_t adds = 0;
    size_t subs = 0;
    size_t max_len = 0;

    s.reserve(100000);

    for(size_t i = 0; i < 1000000; i++)
    {
        std::string t1;
        std::string t2;
        if(rand() % 2)
        {
            // Slow version
            //s += gen_random(t1, (rand() % 15)+3) + "/" + gen_random(t2, (rand() % 15)+3);

            // Fast version
            s += gen_random(t1, (rand() % 15)+3);
            s += "/";
            s += gen_random(t2, (rand() % 15)+3);
            adds++;
        }
        else
        {
            subs++;
            size_t pos = s.find_last_of("/", s.length()-1);
            if(pos != std::string::npos)
                s.resize(pos);

            if(s.length() == 0)
                s = "/";
        }

        if(max_len < s.length())
            max_len = s.length();
    }
    std::cout << "Elapsed: " << clock() - start << std::endl;
    std::cout << "Added: " << adds << std::endl;
    std::cout << "Subtracted: " << subs << std::endl;
    std::cout << "Max: " << max_len << std::endl;

    return 0;
}

在我的系统上,这两者之间大约有1秒的差异(这次使用gcc进行测试,但在Visual Studio中似乎没有任何显着的差异):

Elapsed: 2669
Added: 500339
Subtracted: 499661
Max: 47197

Elapsed: 3417
Added: 500339
Subtracted: 499661
Max: 47367

std::string由于历史原因设计得不太好。许多大型项目在使用自定义字符串类替换其字符串或检查其字符串处理代码并重写以消除临时副本后,发现速度显著提升。 - M.M
是的,我注意到了。在某些情况下,我用vector<char>替换它,这样性能会更好(但代码当然不那么好看了 :) )。 - Devolus
3个回答

2
您的缓慢版本可以重写为:
while(heavyloop)
{
   std::string tempA = path + node;
   std::string tempB = tempA + "/";

   if(tempB == curNode)
   {
        do something
   }
}

是的,它不是一个完全的模拟,但可以使临时对象更加可见。

看看两个临时对象:tempAtempB。它们被创建是因为std::string::operator+总是生成新的std::string对象。这就是std::string的设计方式。编译器无法优化此代码。

C++中有一种技术称为表达式模板来解决这个问题,但同样,它是在库级别上完成的。


你的“重写”并不等同:在OP的代码中,path + node使用"/"调用了它的operator+;没有明显的tempAtempB。更接近的方法是将tempB行更改为:tempA += "/",或者使用tempB = std::move(tempA) + "/" - M.M
代码已经足够接近,只是反过来了。我注意到在最初测试时创建了临时文件,并编译成了 node += "/"; path += node; - Devolus
@M.M 我们无法像获取非 const 引用对象 tempA 一样调用 operator+=,所以代码已经足够接近了。 - Stas
你可以在临时对象上调用运算符。 - M.M
@M.M 是的,但只有const运算符可以,而+=不行,因为它修改了一个对象。无论如何,在代码中存在+时,我们不能调用+=。它们是完全不同的运算符。 - Stas
1
不,非 const 运算符可以在临时对象上调用;而 + 的通常实现是通过调用 += 实现的。 - M.M

2
对于类类型(如std :: string),没有要求必须遵守操作符+和操作符+=之间的传统关系,就像您期望的那样。当然,没有要求a = a + b和a += b具有相同的净效果,因为operator=(),operator+()和operator+=()都可以单独实现,并且不能协同工作。
因此,如果编译器替换了以下代码,则语义上是不正确的:
if(path+node+"/" == curNode)

使用

std::string buffer = path;
buffer += node;
buffer += "/";
if (buffer == curNode)

如果标准中存在一些限制,比如重载的operator+()operator+=()之间有固定的关系,那么这两个代码片段将具有相同的净效果。然而,没有这样的限制,因此编译器不允许进行这样的替换。结果会改变代码的含义。


如果两个(明确定义的)代码片段可能具有不同的含义,编译器就不能将一个替换为另一个。 - Peter

1
"

path+node+"/"会分配一个临时变量字符串与curNode进行比较,这是C++的实现。

"

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接