【问题标题】:Using .size() vs const variable for loops使用 .size() 与 const 变量进行循环
【发布时间】:2015-06-20 08:59:09
【问题描述】:

我有一个vector

vector<Body*> Bodies;

它包含指向我已定义的Body 对象的指针。

我还有一个unsigned int const,其中包含我希望在bodies 中拥有的bodyobjects 的数量。

unsigned int const NumParticles = 1000;

我已经用NumParticles 填充了Bodies 数量的Body 对象。

现在,如果我想遍历一个循环,例如调用Bodies 中的每个Body 的Update() 函数,我有两种选择:

第一:

for (unsigned int i = 0; i < NumParticles; i++)
{
    Bodies.at(i)->Update();
}

或者第二个:

for (unsigned int i = 0; i < Bodies.size(); i++)
{
    Bodies.at(i)->Update();
}

各有优缺点。我想知道在安全性、可读性和惯例方面,哪一个(如果有的话)是更好的做法。

【问题讨论】:

  • “我想知道哪一个(如果有的话)在安全性、可读性和惯例方面会更好。” Bodies.size() 当然。

标签: c++ vector coding-style constants


【解决方案1】:

我希望,鉴于编译器(至少在这种情况下)可以内联 std::vector 中的所有相关代码,它将是相同的代码 [除了 1000 是机器代码中的真正常量文字,而 @987654322 @ 将是一个“变量”值]。

调查结果的简短摘要:

  • 编译器不会为每次迭代的向量的size() 调用函数,它会在循环开始时计算它,并将其用作“常量值”。

  • 循环中的实际代码是相同的,只是循环的准备不同。

  • 一如既往:如果性能非常重要,请使用您的数据和编译器在您的系统上进行测量。否则,编写对您的设计最有意义的代码(我更喜欢使用for(auto i : vec),因为这很简单[并且适用于所有容器])

支持证据:

拿完咖啡后,我写了这段代码:

class X
{
public:
    void Update() { x++; }
    operator int() { return x; }
private:
    int x = rand();
};

extern std::vector<X*> vec;
const size_t vec_size = 1000;

void Process1()
{
    for(auto i : vec)
    {
        i->Update();
    }
}

void Process2()
{
    for(size_t i = 0; i < vec.size(); i++)
    {
        vec[i]->Update();
    }
}


void Process3()
{
    for(size_t i = 0; i < vec_size; i++)
    {
        vec[i]->Update();
    }
}

(以及填充数组并调用 Process1()、Process2() 和 Process3() 的 main 函数 - main 位于单独的文件中,以避免编译器决定内联所有内容并使其很难说是什么)

这是g++ 4.9.2生成的代码:

0000000000401940 <_Z8Process1v>:
  401940:   48 8b 0d a1 18 20 00    mov    0x2018a1(%rip),%rcx        # 6031e8 <vec+0x8>
  401947:   48 8b 05 92 18 20 00    mov    0x201892(%rip),%rax        # 6031e0 <vec>
  40194e:   48 39 c1                cmp    %rax,%rcx
  401951:   74 14                   je     401967 <_Z8Process1v+0x27>
  401953:   0f 1f 44 00 00          nopl   0x0(%rax,%rax,1)
  401958:   48 8b 10                mov    (%rax),%rdx
  40195b:   48 83 c0 08             add    $0x8,%rax
  40195f:   83 02 01                addl   $0x1,(%rdx)
  401962:   48 39 c1                cmp    %rax,%rcx
  401965:   75 f1                   jne    401958 <_Z8Process1v+0x18>
  401967:   f3 c3                   repz retq 

0000000000401970 <_Z8Process2v>:
  401970:   48 8b 35 69 18 20 00    mov    0x201869(%rip),%rsi        # 6031e0 <vec>
  401977:   48 8b 0d 6a 18 20 00    mov    0x20186a(%rip),%rcx        # 6031e8 <vec+0x8>
  40197e:   31 c0                   xor    %eax,%eax
  401980:   48 29 f1                sub    %rsi,%rcx
  401983:   48 c1 f9 03             sar    $0x3,%rcx
  401987:   48 85 c9                test   %rcx,%rcx
  40198a:   74 14                   je     4019a0 <_Z8Process2v+0x30>
  40198c:   0f 1f 40 00             nopl   0x0(%rax)
  401990:   48 8b 14 c6             mov    (%rsi,%rax,8),%rdx
  401994:   48 83 c0 01             add    $0x1,%rax
  401998:   83 02 01                addl   $0x1,(%rdx)
  40199b:   48 39 c8                cmp    %rcx,%rax
  40199e:   75 f0                   jne    401990 <_Z8Process2v+0x20>
  4019a0:   f3 c3                   repz retq 

00000000004019b0 <_Z8Process3v>:
  4019b0:   48 8b 05 29 18 20 00    mov    0x201829(%rip),%rax        # 6031e0 <vec>
  4019b7:   48 8d 88 40 1f 00 00    lea    0x1f40(%rax),%rcx
  4019be:   66 90                   xchg   %ax,%ax
  4019c0:   48 8b 10                mov    (%rax),%rdx
  4019c3:   48 83 c0 08             add    $0x8,%rax
  4019c7:   83 02 01                addl   $0x1,(%rdx)
  4019ca:   48 39 c8                cmp    %rcx,%rax
  4019cd:   75 f1                   jne    4019c0 <_Z8Process3v+0x10>
  4019cf:   f3 c3                   repz retq 

虽然每种情况的汇编代码看起来略有不同,但实际上,我想说你很难衡量这些循环之间的差异,事实上,在代码上运行 perf表明它是“所有循环的同一时间”[这是在一个循环中有 100000 个元素和对 Process1、Process2 和 Process3 的 100 次调用,否则时间由main 中的new X 主导]:

  31.29%  a.out    a.out                [.] Process1
  31.28%  a.out    a.out                [.] Process3
  31.13%  a.out    a.out                [.] Process2

除非您认为 1/10 的百分比很重要 - 并且可能需要一周的时间才能运行,但这只是十分之几秒 [在我的机器上为 0.163 秒],并且可能需要更多测量错误比其他任何东西都少 - 更短的时间实际上是理论上应该是最慢的,Process2,使用vec.size()。我用更高的循环数再次运行,现在每个循环的测量值是彼此的 0.01% - 换句话说,花费的时间相同。

当然,如果你仔细看,你会发现所有三个变体的实际循环内容基本相同,除了Process3 的早期部分更简单,因为编译器知道我们至少会做一个循环 - Process1Process2 必须在第一次迭代之前检查“向量是否为空”。这将对非常短的向量长度产生影响。

【讨论】:

  • 哇。其实哇,我屏住了呼吸。当我提出这个问题时,我预计要么对“nooby”问题做出负面回应,要么根本没有回应。您在此答案中所做的工作令人惊叹。先生,您应得的所有代表
【解决方案2】:

我会为范围投票

for (auto* body : Bodies)
{
    body->Update();
}

【讨论】:

  • 您的意思是写body-&gt;Update(); insted of Body-&gt;Update();
【解决方案3】:

NumParticles 不是向量的属性。它是相对于向量的一些外部常数。我更喜欢使用向量的属性size()。在这种情况下,代码对读者来说更加安全和清晰。

通常使用一些常量而不是size() 意味着读者通常可以不等于size()

因此,如果您想告诉读者您要处理向量的所有元素,那么最好使用 size()。否则使用常量。

当然,当重音放在常量上时,这个隐含规则也有例外。在这种情况下,最好使用常量。但这取决于上下文。

【讨论】:

    【解决方案4】:

    我建议你使用.size() 函数而不是定义一个新的常量。

    为什么?

    1. 安全:由于.size() 不会抛出任何异常,因此使用.size() 是绝对安全的。

    2. 可读性:恕我直言,Bodies.size()NumParticles 更清楚地传达了向量 Bodies 的大小。

    3. 约定:根据约定,最好使用.size(),因为它是向量的属性,而不是变量NumParticles

    4. 性能:.size() 是一个恒定复杂度成员函数,因此使用const int.size() 之间没有显着的性能差异。

    【讨论】:

      【解决方案5】:

      我更喜欢这种形式:

      for (auto const& it : Bodies)
      {
          it->Update();
      }
      

      【讨论】:

        猜你喜欢
        • 2019-09-27
        • 1970-01-01
        • 2016-01-10
        • 1970-01-01
        • 1970-01-01
        • 2015-10-05
        • 2019-05-22
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多