【问题标题】:Performance optimization for std::stringstd::string 的性能优化
【发布时间】:2015-10-27 05:55:20
【问题描述】:

当我在我的应用中进行一些性能测试时,我注意到以下代码 (Visual Studio 2010) 有所不同。

较慢的版本

while(heavyloop)
{
   if(path+node+"/" == curNode)
   {
        do something
   }
}

这将导致生成结果字符串的一些额外 malloc。

为了避免这些mallocs,我改成如下方式:

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 设计得不是很好,由于历史原因。许多大型项目在用自定义字符串类替换字符串或检查字符串处理代码并重写以消除临时副本后发现了相当大的速度。
  • 是的,我注意到了。在某些情况下,我将其替换为表现更好的 vector(但代码当然没有那么好。:))。

标签: c++ optimization


【解决方案1】:

你的慢版本可能会被重写为

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++ 中有一种称为 expression templates 的技术来解决这个问题,但同样,它是在库级别完成的。

【讨论】:

  • 您的“重写”不等效:在 OP 的代码中,path + nodeoperator+"/" 调用;没有明显的tempAtempB。更接近于将tempB 行更改为:tempA += "/".,或使用tempB = std::move(tempA) + "/"
  • 代码足够接近,只是颠倒了。我在最初测试时注意到创建了临时文件,并将其编译为node += "/"; path += node;
  • @M.M 我们不能为tempA 调用operator+=,就像我们不能获得tempA 对象的非const 引用一样,所以代码足够接近。
  • 你可以在临时对象上调用操作符
  • 不,可以在临时对象上调用非常量运算符;而+的通常实现是通过调用+=实现的
【解决方案2】:

对于类类型(如std::string),不需要像您期望的那样遵守运算符+ 和运算符+= 之间的常规关系。当然没有要求a = a + ba += b 具有相同的净效果,因为operator=()operator+()operator+=() 都可以单独实现,而不是串联在一起。

因此,如果被替换,编译器在语义上会不正确

if(path+node+"/" == curNode)

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

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

【讨论】:

  • 如果两个(定义明确的)代码片段可能具有不同的含义,则编译器不能用一个代替另一个。
【解决方案3】:

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2012-04-23
    • 2018-11-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-11-04
    • 2020-09-17
    • 1970-01-01
    相关资源
    最近更新 更多