【问题标题】:Constructor initialization speed构造函数初始化速度
【发布时间】:2018-08-15 09:20:38
【问题描述】:

我运行了一些测试来验证我对构造函数速度的先入为主的想法,但结果与我的预期大不相同。难道我做错了什么?我错过了什么? 我正在使用没有优化的 MVS 2015(但结果始终相同并不重要)。

class Person
{   using ushort = unsigned short;
    string  name_;
    ushort  age_;    
public: 
    explicit Person(const string &name, ushort age) //1
    {   name_ = name;
        age_  = age;
    }        
    explicit Person(const string &name,ushort age) : name_{name},age_{age} //2
    {   
    }        
    explicit Person(string name, ushort age) : name_{ std::move(name) }, age_{ age } //3
    {   
    }
};

//1 - 总是比其他 ctor 快。

//2 - 比 1 稍慢(可以忽略!!)。???

//3 - 比 1 多花 50% 的时间。???

我使用小字符串(8 字节)、1K、2K 字符串大小运行测试。 我期待 //2 ctor 是最快的,没想到 //3 花了这么多时间。 最后一个问题,我在 //3 ctor 中所做的事情是否合法?

更新 这是我写的代码

class Person
{
using ushort = unsigned short;
private:
    std::string name_;
    ushort      age_;
public:
    static constexpr size_t nr_ctors = 3;
    static const char * const ctor_signature[nr_ctors];

    enum class CTOR_1
    {   CTOR = 0
    };
    enum class CTOR_2
    {   CTOR = 1
    };
    enum class CTOR_3
    {   CTOR = 2
    };

    explicit Person(const std::string &name, ushort age,CTOR_1) 
    {   name_ = name;
        age_  = age;
    }
    explicit Person(const std::string &name, ushort age, CTOR_2) : name_{name},age_{age}
    {}
    explicit Person(std::string name,ushort age,CTOR_3) : name_{std::move(name)},age_{age}
    {}
};

const char * const Person::ctor_signature[Person::nr_ctors] = {"\nexplicit Person(const std::string &name, ushort age,CTOR_1)",
                                                               "\nexplicit Person(const std::string &name, ushort age, CTOR_2) : name_{name},age_{age}",
                                                               "\nexplicit Person(std::string name,ushort age,CTOR_3) : name_{std::move(name)},age_{age}"};

using mclock        = std::chrono::high_resolution_clock;
using time_p_t      = std::chrono::time_point<mclock>;
using precision_t   = std::chrono::nanoseconds;

#define NR_ITERATIONS   (128 * 1024)

template <typename Ty_>
precision_t time_no_heap(const std::string &name)
{
    time_p_t t_0;
    time_p_t t_1;

    t_0 = mclock::now();
    Person p = Person{name,66,Ty_::CTOR};
    t_1 = mclock::now();

    return t_1 - t_0;
}

template <typename Ty_>
precision_t time_with_heap(const std::string &name)
{
    time_p_t t_0;
    time_p_t t_1;
    Person   *p_person;

    t_0 = mclock::now();
    p_person = new Person{ name,66,Ty_::CTOR };
    t_1 = mclock::now();
    delete p_person;

    return t_1 - t_0;
}

void print_statistics(int iterations, size_t str_size, const precision_t(&stats)[2][Person::nr_ctors])
{
    std::cout << "\nTotal iterations : " 
              << iterations
              << "\nString ize : "
              << str_size
              << std::endl;
    for (int i = 0; i < Person::nr_ctors; ++i)
    {   std::cout << Person::ctor_signature[i]
                  << "\n\t Stack (ms) : " 
                  << std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(stats[0][i]).count()
                  << "\n\t Heap (ms)  : "
                  << std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(stats[1][i]).count()
                  << std::endl;
    }
}

int main(int argc, const char *argv[])
{   
    int         iterations;
    std::string *p_name;

    if (argc != 3 && argc != 1)
    {   std::cout << "USAGE [<iterations>K <string size>]" << std::endl;
        return -1;
    }
    else if (argc == 3)
    {   iterations = std::atoi(argv[1]) * 1024;
        p_name = new std::string(std::atoi(argv[2]), 'x');
    }
    else
    {   iterations = NR_ITERATIONS;
        p_name = new std::string{ "Benchmark" };
    }

    precision_t benchmark  [2][Person::nr_ctors]{};

    std::cout << "\nUsing string : " << *p_name << ".\nIterating : " << iterations << " times." << std::endl;

    for (auto i = iterations; --i >= 0; )
    {   //Stack allocation
        benchmark[0][(int)Person::CTOR_1::CTOR] += time_no_heap<Person::CTOR_1>(*p_name);
        benchmark[0][(int)Person::CTOR_2::CTOR] += time_no_heap<Person::CTOR_2>(*p_name);
        benchmark[0][(int)Person::CTOR_3::CTOR] += time_no_heap<Person::CTOR_3>(*p_name);
        //Heap allocation
        benchmark[1][(int)Person::CTOR_1::CTOR] += time_with_heap<Person::CTOR_1>(*p_name);
        benchmark[1][(int)Person::CTOR_2::CTOR] += time_with_heap<Person::CTOR_2>(*p_name);
        benchmark[1][(int)Person::CTOR_3::CTOR] += time_with_heap<Person::CTOR_3>(*p_name);
    }

    print_statistics(iterations,p_name->size(),benchmark);

    delete p_name;

    return 0;
}

我被 MVS 的 debug 版本弄错了,它显示的结果与 release 版本完全不同,即使项目设置相同!我的错..

【问题讨论】:

  • 没有优化的基准测试是没有意义的。另外,你的基准是什么样的?你以name 传递什么字符串(它是否适合SBO)?
  • 嗨,也许在那里运行它:quick-bench.com 你会看到程序集以了解程序实际在做什么。您也可以使用其他编译器和优化进行测试。我做了一些基本测试,我发现了结果与您的不同

标签: c++11 visual-c++ visual-studio-2015 constructor move-semantics


【解决方案1】:

没有优化的基准测试毫无意义。

你以name 传递什么字符串?一个很重要的因素是字符串的长度,以及是否会触发SBO(小缓冲区优化)。

如果字符串足够短,移动不会比复制快。

另外,您的基准是什么样的?可能是您的基准测试代码有缺陷。

【讨论】:

    【解决方案2】:

    我也希望 //2 是最快的,让我们看看构造函数的不同之处。

    • //1:name_ 是默认构造的,然后复制分配。
    • //2:name_ 是复制构造的。
    • //3:name是copy构造的,name_是move构造的。

    随着默认构造变得更加昂贵,//1 和//2 之间的差异会越来越大。

    //3 是有效的,因为参数名称只存在于构造函数中,因此我们可以通过移动构造名称_来窃取内部信息。

    Nicolai Josuttis 对此有来自 CppCon 2017 的 great talk

    【讨论】:

      【解决方案3】:

      当我尝试对您的代码进行基准测试时,我得到了一些不同的结果。 基准测试在使用 Clang 5.0 C++17 -O3 编译的 AWS 机器池上运行。

      #include <string>
      
      class Person1
      {   using ushort = unsigned short;
          std::string  name_;
          ushort  age_;    
      public: 
          explicit Person1(const std::string &name, ushort age) //1
          {   name_ = name;
              age_  = age;
          }  
      };
      
      class Person2
      {   using ushort = unsigned short;
          std::string  name_;
          ushort  age_;    
      public:            
          explicit Person2(const std::string &name,ushort age) : name_{name},age_{age} //2
          {   
          }    
      };
      
      class Person3
      {   using ushort = unsigned short;
          std::string  name_;
          ushort  age_;    
      public:    
          explicit Person3(std::string name, ushort age) : name_{ std::move(name) }, age_{ age } //3
          {   
          }
      };
      
      
      static void CreatePerson1(benchmark::State& state) 
      {
         for (auto _ : state) {
             Person1 person("Hello World!!!!!!!!!!!!", 10);
         }
      }
      BENCHMARK(CreatePerson1);
      
      static void CreatePerson2(benchmark::State& state) 
      {
          for (auto _ : state) {
             Person2 person("Hello World!!!!!!!!!!!!", 10);
          }
      }
      BENCHMARK(CreatePerson2);
      
      static void CreatePerson3(benchmark::State& state) 
      {
          for (auto _ : state) {
             Person3 person("Hello World!!!!!!!!!!!!", 10);
          }
      }
      BENCHMARK(CreatePerson3);
      
      1. 对于一个小字符串(8 字节),第二个构造函数是最快的,其次是第三个。在此处查看结果Quick C++ Benchmarks

      2. 对于较大的字符串(20 字节),第三个构造函数最快,其次是第二个。在此处查看结果Quick C++ Benchmarks

      【讨论】:

        【解决方案4】:

        您在第三种情况下制作副本。取而代之的是,尝试这样的事情:

        class Person
        {   using ushort = unsigned short;
            string  name_;
            ushort  age_;    
        public:     
            explicit Person(string&& name, ushort age) : name_{ std::move(name) }, age_{ age } //3
            {   
            }
            explicit Person(const string &name, ushort age) //1
            {  
                name_ = name;
                age_  = age;
            }        
            explicit Person(const string &name,ushort age) : name_{name},age_{age} //2
            {   
            }  
        };
        

        使用 R 值必须提高性能。

        或者您可以像这样为 aboce r-value 和 l-value 创建一个构造函数:

        class Person
        {   using ushort = unsigned short;
            string  name_;
            ushort  age_;    
        public:   
            template<typename T>  
            explicit Person(T&& name, ushort age) : name_{ std::forward<T>(name) }, age_{ age } //3
            {   
            }
        
        };
        

        【讨论】:

        • 这是错误的。即使name 不是右值引用,用户也可以将字符串移动到name
        • @VittorioRomeo,这里已经存在构造函数,接受左值参数
        • @VittorioRomeo 有没有 cmets?
        • 这仍然是错误的。按值取值并不意味着复制,因为调用者可以将某些内容移动到按值取值的参数中。此外,您的完美转发版本应该采用T&amp;&amp;,而不是T
        • @Vittorio Romeo I 修复了转发版本。我还是不明白你说的An user can move a string into name even if name is not an rvalue reference.是什么意思
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2015-08-08
        • 2018-06-30
        • 2019-10-07
        • 2013-11-17
        • 1970-01-01
        相关资源
        最近更新 更多