【问题标题】:Cost of thread-safe local static variable initialization in C++11?C ++ 11中线程安全的局部静态变量初始化的成本?
【发布时间】:2016-07-15 08:07:26
【问题描述】:

我们知道局部静态变量初始化在 C++11 中是线程安全的,现代编译器完全支持这一点。 (Is local static variable initialization thread-safe in C++11?)

使它成为线程安全的成本是多少?我知道这很可能取决于编译器实现。

上下文:我有一个多线程应用程序(10 个线程)通过以下函数以非常高的速率访问单例对象池实例,我担心它对性能的影响。

template <class T>
ObjectPool<T>* ObjectPool<T>::GetInst()
{
    static ObjectPool<T> instance;
    return &instance;
}

【问题讨论】:

  • 只是一个警告:当您的应用程序退出时,将调用静态变量的构造函数。包括其他线程仍在使用的单例对象。
  • 用另一种称为“双重检查锁定”的已知技术和原子来测量它怎么样?然后你会有一些基准,并对成本做出有根据的猜测。
  • @Arunmu 好主意,我试试看。我希望有人能阐明编译器是如何实际实现它的。
  • 将它传递给线程的参数,所以这部分没有任何开销。
  • @Sunanda 它的实现方式基本上取决于编译器和平台。但是,据我所知,他们中的大多数都使用某种形式的 DCLP 来实现线程安全。因此,可以肯定地假设它比您能想出的任何 DCLP 版本都更有效。

标签: c++ c++11 thread-safety static-initialization


【解决方案1】:

A look at the generated assembler code 帮助。

来源

#include <vector>

std::vector<int> &get(){
  static std::vector<int> v;
  return v;
}
int main(){
  return get().size();
}

汇编器

std::vector<int, std::allocator<int> >::~vector():
        movq    (%rdi), %rdi
        testq   %rdi, %rdi
        je      .L1
        jmp     operator delete(void*)
.L1:
        rep ret
get():
        movzbl  guard variable for get()::v(%rip), %eax
        testb   %al, %al
        je      .L15
        movl    get()::v, %eax
        ret
.L15:
        subq    $8, %rsp
        movl    guard variable for get()::v, %edi
        call    __cxa_guard_acquire
        testl   %eax, %eax
        je      .L6
        movl    guard variable for get()::v, %edi
        movq    $0, get()::v(%rip)
        movq    $0, get()::v+8(%rip)
        movq    $0, get()::v+16(%rip)
        call    __cxa_guard_release
        movl    $__dso_handle, %edx
        movl    get()::v, %esi
        movl    std::vector<int, std::allocator<int> >::~vector(), %edi
        call    __cxa_atexit
.L6:
        movl    get()::v, %eax
        addq    $8, %rsp
        ret
main:
        subq    $8, %rsp
        call    get()
        movq    8(%rax), %rdx
        subq    (%rax), %rdx
        addq    $8, %rsp
        movq    %rdx, %rax
        sarq    $2, %rax
        ret

对比

来源

#include <vector>

static std::vector<int> v;
std::vector<int> &get(){
  return v;
}
int main(){
  return get().size();
}

汇编器

std::vector<int, std::allocator<int> >::~vector():
        movq    (%rdi), %rdi
        testq   %rdi, %rdi
        je      .L1
        jmp     operator delete(void*)
.L1:
        rep ret
get():
        movl    v, %eax
        ret
main:
        movq    v+8(%rip), %rax
        subq    v(%rip), %rax
        sarq    $2, %rax
        ret
        movl    $__dso_handle, %edx
        movl    v, %esi
        movl    std::vector<int, std::allocator<int> >::~vector(), %edi
        movq    $0, v(%rip)
        movq    $0, v+8(%rip)
        movq    $0, v+16(%rip)
        jmp     __cxa_atexit

我对汇编程序不是很好,但我可以看到在第一个版本中v 有一个锁,get 没有内联,而在第二个版本中get 基本上没有了。
您可以 play around 使用各种编译器和优化标志,但似乎没有编译器能够内联或优化锁,即使程序显然是单线程的。
您可以将static 添加到get,这会使gcc 内联get,同时保留锁。

要了解这些锁和额外指令对您的编译器、标志、平台和周围代码的成本,您需要进行适当的基准测试。
我预计锁会有一些开销并且比内联代码慢得多,当您实际使用向量时,这变得微不足道,但如果不进行测量,您永远无法确定。

【讨论】:

  • 这里static的含义与函数中的静态变量不同!这里它只是一个局部范围的变量,在 C++ 中你不应该再使用它。所以这根本不能回答问题!
  • @DrumM 我们不是在比较 static 的含义,我们是在比较魔术静态与非魔术静态。将v 设为全局是一种合理的方式来禁用魔法静态,同时让其余部分或多或少保持不变。另一种方法是从原始代码中删除static,但我觉得这会进一步改变含义。您是否有更好的想法来禁用魔法静态而不更改任何其他内容?
  • 但是你在这里比较的是?s和?s……正如所说的全局静态的含义与函数中的静态变量不同,所以它的魔力也不同。问题是static,没有别的办法。
  • “正如所说的,全局静态的含义与函数中的静态变量不同,所以它的魔力也不同。”是的。这就是重点。就像我之前说的,我们想比较一个魔法静态和一个非魔法静态。进行这种比较需要不同的魔法。问题不是“魔法静电有什么作用?”,而是“魔法静电的成本是多少?”并且通过比较魔法静态和非魔法静态来回答对我来说似乎是完全合理的。如果全局静态具有相同的魔法属性,那么比较它们就没有意义了。
【解决方案2】:

根据我的经验,这与常规互斥锁(关键部分)的成本完全相同。如果代码被非常频繁地调用,请考虑使用普通的全局变量。

【讨论】:

  • 代码看起来像GetInst 是类ObjectPool 的一部分,这意味着建议的全局变量可以改为类中的private static 变量。只要T 不访问其构造函数中的其他全局变量就可以了。
  • 谢谢,看来这是避免任何同步开销的方法
【解决方案3】:

由 Jason Turner 在https://www.youtube.com/watch?v=B3WWsKFePiM 进行了广泛的解释。

我放了一个示例代码来说明视频。由于线程安全是主要问题,因此我尝试从多个线程调用该方法以查看其效果。

你可以认为编译器正在为你实现双重检查锁,即使他们可以做任何他们想做的事情来确保线程安全。但是他们至少会添加一个分支来区分第一次初始化,除非优化器在全局范围内急切地进行初始化。

https://en.wikipedia.org/wiki/Double-checked_locking#Usage_in_C++11

#include <iostream>
#include <string>
#include <vector>
#include <thread>

struct Temp
{
  // Everytime this method is called, compiler has to check whether `name` is
  // constructed or not due to init-at-first-use idiom. This at least would 
  // involve an atomic load operation and maybe a lock acquisition.
  static const std::string& name() {
    static const std::string name = "name";
    return name;
  }

  // Following does not create contention. Profiler showed little bit of
  // performance improvement.
  const std::string& ref_name = name();
  const std::string& get_name_ref() const {
    return ref_name;
  }
};

int main(int, char**)
{
  Temp tmp;

  constexpr int num_worker = 8;
  std::vector<std::thread> threads;
  for (int i = 0; i < num_worker; ++i) {
    threads.emplace_back([&](){
      for (int i = 0; i < 10000000; ++i) {
        // name() is almost 5s slower
        printf("%zu\n", tmp.get_name_ref().size());
      }
    });
  }

  for (int i = 0; i < num_worker; ++i) {
    threads[i].join();
  }

  return 0;
}

name() 版本比我机器上的 get_name_ref() 慢 5s。

$ time ./test > /dev/null

我还使用编译器资源管理器查看 gcc 生成了什么。以下证明了双重检查锁模式:注意获取的原子负载和守卫。

name ()
{
  bool retval.0;
  bool retval.1;
  bool D.25443;
  struct allocator D.25437;
  const struct string & D.29013;
  static const struct string name;

  _1 = __atomic_load_1 (&_ZGVZL4namevE4name, 2);
  retval.0 = _1 == 0;
  if (retval.0 != 0) goto <D.29003>; else goto <D.29004>;
  <D.29003>:
  _2 = __cxa_guard_acquire (&_ZGVZL4namevE4name);
  retval.1 = _2 != 0;
  if (retval.1 != 0) goto <D.29006>; else goto <D.29007>;
  <D.29006>:
  D.25443 = 0;
  try
    {
      std::allocator<char>::allocator (&D.25437);
      try
        {
          try
            {
              std::__cxx11::basic_string<char>::basic_string (&name, "name", &D.25437);
              D.25443 = 1;
              __cxa_guard_release (&_ZGVZL4namevE4name);
              __cxa_atexit (__dt_comp , &name, &__dso_handle);
            }
          finally
            {
              std::allocator<char>::~allocator (&D.25437);
            }
        }
      finally
        {
          D.25437 = {CLOBBER};
        }
    }
  catch
    {
      if (D.25443 != 0) goto <D.29008>; else goto <D.29009>;
      <D.29008>:
      goto <D.29010>;
      <D.29009>:
      __cxa_guard_abort (&_ZGVZL4namevE4name);
      <D.29010>:
    }
  goto <D.29011>;
  <D.29007>:
  <D.29011>:
  goto <D.29012>;
  <D.29004>:
  <D.29012>:
  D.29013 = &name;
  return D.29013;
}

【讨论】:

    猜你喜欢
    • 2011-12-27
    • 2017-03-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-12-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多