【问题标题】:Multi-threaded performance std::string多线程性能 std::string
【发布时间】:2011-01-27 15:07:32
【问题描述】:

我们正在一个使用 OpenMP 的项目上运行一些代码,但我遇到了一些奇怪的事情。我已经包含了一些演示代码的部分内容。

测试比较在多线程循环中调用具有 const char* 参数的函数和 std::string 参数。这些函数基本上什么都不做,因此没有开销。

我所看到的是完成循环所需的时间存在重大差异。对于执行 100,000,000 次迭代的 const char* 版本,代码需要 0.075 秒才能完成,而 std::string 版本需要 5.08 秒。这些测试是在带有 gcc-4.4 的 Ubuntu-10.04-x64 上完成的。

我的问题基本上是这是否仅仅是由于 std::string 的动态分配以及为什么在这种情况下不能优化掉,因为它是 const 并且不能改变?

下面的代码,非常感谢您的回复。

编译:g++ -Wall -Wextra -O3 -fopenmp string_args.cpp -o string_args

#include <iostream>
#include <map>
#include <string>
#include <stdint.h>

// For wall time
#ifdef _WIN32
#include <time.h>
#else
#include <sys/time.h>
#endif

namespace
{
  const int64_t g_max_iter = 100000000;
  std::map<const char*, int> g_charIndex = std::map<const char*,int>();
  std::map<std::string, int> g_strIndex = std::map<std::string,int>();

  class Timer
  {
  public:
    Timer()
    {
    #ifdef _WIN32
      m_start = clock();
    #else /* linux & mac */
      gettimeofday(&m_start,0);
    #endif
    }

    float elapsed()
    {
    #ifdef _WIN32
      clock_t now = clock();
      const float retval = float(now - m_start)/CLOCKS_PER_SEC;
      m_start = now;
    #else /* linux & mac */
      timeval now;
      gettimeofday(&now,0);
      const float retval = float(now.tv_sec - m_start.tv_sec) + float((now.tv_usec - m_start.tv_usec)/1E6);
      m_start = now;
    #endif
      return retval;
    }

  private:
    // The type of this variable is different depending on the platform
#ifdef _WIN32
    clock_t
#else
    timeval
#endif
    m_start;   ///< The starting time (implementation dependent format)
  };

}

bool contains_char(const char * id)
{
  if( g_charIndex.empty() ) return false;
  return (g_charIndex.find(id) != g_charIndex.end());
}

bool contains_str(const std::string & name)
{
  if( g_strIndex.empty() ) return false;
  return (g_strIndex.find(name) != g_strIndex.end());
}

void do_serial_char()
{
  int found(0);
  Timer clock;
  for( int64_t i = 0; i < g_max_iter; ++i )
  {
    if( contains_char("pos") )
    {
     ++found;
    }
  }
  std::cout << "Loop time: " << clock.elapsed() << "\n";
  ++found;
}

void do_parallel_char()
{
  int found(0);
  Timer clock;
#pragma omp parallel for
  for( int64_t i = 0; i < g_max_iter; ++i )
  {
    if( contains_char("pos") )
    {
     ++found;
    }
  }
  std::cout << "Loop time: " << clock.elapsed() << "\n";
  ++found;
}

void do_serial_str()
{
  int found(0);
  Timer clock;
  for( int64_t i = 0; i < g_max_iter; ++i )
  {
    if( contains_str("pos") )
    {
     ++found;
    }
  }
  std::cout << "Loop time: " << clock.elapsed() << "\n";
  ++found;
}

void do_parallel_str()
{
  int found(0);
  Timer clock;
#pragma omp parallel for
  for( int64_t i = 0; i < g_max_iter ; ++i )
  {
    if( contains_str("pos") )
    {
     ++found;
    }
  }
  std::cout << "Loop time: " << clock.elapsed() << "\n";
  ++found;
}

int main()
{
  std::cout << "Starting single-threaded loop using std::string\n";
  do_serial_str();
  std::cout << "\nStarting multi-threaded loop using std::string\n";
  do_parallel_str();

  std::cout << "\nStarting single-threaded loop using char *\n";
  do_serial_char();
  std::cout << "\nStarting multi-threaded loop using const char*\n";
  do_parallel_char();
  }

【问题讨论】:

  • 在没有 OpenMP 的情况下看到这个时间会很有趣。还有 contains_str 和 contains_char 的代码以及编译器标志,因此我们可以自己重复实验..
  • 您是否尝试过调试和发布二进制文件?
  • 不比较苹果和苹果。将字符串结构移出循环以获得等效比较。
  • 如果没有 OpenMP,std::string 版本大约需要 5.5 秒,我猜这也是分配。我没有拉出字符串构造的原因是这是我想模仿我们真实代码的玩具代码。在实际版本中,对接受参数的函数的调用显然不在循环内,因此取出 std::string 构造并不是很明显的事情。无论如何,我实际上应该将其删除,因为“pos”已被写在很多地方。感谢 cmets。
  • 对于那些有兴趣的人,是否可以附加一个文件来发布或者我应该把它复制进去(这是我的第一个问题:))?

标签: c++


【解决方案1】:

我的问题基本上是这是否仅仅是由于 std::string 的动态分配以及为什么在这种情况下不能优化掉,因为它是 const 并且不能改变?

是的,这是由于每次迭代都为 std::string 分配和复制。

sufficiently smart compiler 可能会对此进行优化,但目前的优化器不太可能发生这种情况。相反,您可以自己吊起绳子:

void do_parallel_str()
{
  int found(0);
  Timer clock;
  std::string const str = "pos";  // you can even make it static, if desired
#pragma omp parallel for
  for( int64_t i = 0; i < g_max_iter; ++i )
  {
    if( contains_str(str) )
    {
      ++found;
    }
  }
  //clock.stop();  // Or use something to that affect, so you don't include
  // any of the below expression (such as outputing "Loop time: ") in the timing.
  std::cout << "Loop time: " << clock.elapsed() << "\n";
  ++found;
}

【讨论】:

  • 在 STL 之前我做了一个 COW 字符串类。我试图有效地处理“将这个常量字符数组转换为字符串”的情况,但它很麻烦,仍然不如不一直创建字符串那么有效。
  • @Omnifarious:问题是您无法自动区分字符串文字和“普通”字符数组。但是一些字符串库可以让你手动区分,比如 contains_str(some_string_type::from_literal("pos"))。想象一个几乎完美的(出于这个问题的目的)COW 字符串类型允许这样做,它将非常接近没有开销,但仍然会有一些。我认为 C++0x 中的 constexpr 在这种假设情况下有可能将开销减少到零。
  • 我使用默认为 false 的 bool 构造函数参数处理它。如果字符串是文字,则将其设置为 true
  • @Omnifarious:我更喜欢工厂方法而不是分散在使用中的匿名真实值,但结果相同。
【解决方案2】:

变化:

if( contains_str("pos") )

到:

static const std::string str = "pos";
if( str )

改变很多?我目前最好的猜测是,每个循环对std::string 的隐式构造函数调用会引入相当多的开销,并且在可能的情况下对其进行优化仍然是我怀疑的一个足够困难的问题。

【讨论】:

    【解决方案3】:

    std::string(在您的情况下是临时的)需要动态分配,与循环中的其他所有操作相比,这是一个非常缓慢的操作。也有做 COW 的标准库的旧实现,在多线程环境中也很慢。话虽如此,编译器没有理由不能优化临时字符串的创建并优化整个contains_str 函数调用,除非你有一些副作用。由于您没有提供该功能的实现,因此无法说它是否可以完全优化掉。

    【讨论】:

    • 如果我们假设样本是有代表性的,编译器也不能看到函数体,或者基于这样的优化(它必须假设函数可以使用和改变全局州)。
    • @Mark,编译器多年来一直在优化整个程序。无需在同一个 TU 中查看定义即可优化调用。
    猜你喜欢
    • 1970-01-01
    • 2021-09-08
    • 1970-01-01
    • 1970-01-01
    • 2012-04-23
    • 2012-09-01
    • 2023-01-11
    相关资源
    最近更新 更多