【问题标题】:Is head-tail printing more efficient than checking for end or beginning?头尾打印是否比检查结束或开始更有效?
【发布时间】:2014-03-04 07:02:05
【问题描述】:

所以我有一个清单。我想打印该列表中用空格分隔的所有元素。

我首先想到的是做,

for (auto& ele : somelist)
{
  cout << ele << ' ';
}

或者,

for (auto& ele : somelist)
{
  cout << ' ' << ele;
}

问题在于引入了额外的空间。

因此,处理多余空间的一种方法是使用条件。

for (int idx{}; idx < somelist.size(); ++idx)
{
  if (idx == 0)
    cout << somelist[idx];

  else
    cout << ' ' << somelist[idx];
}

或者,

for (int idx{}; idx < somelist.size(); ++idx)
{
  if (idx == somelist.size() - 1)
    cout << somelist[idx];

  else
    cout << somelist[idx] << ' ';
}

但是有一个条件只有一次才会成立,这让我很困扰。我想出了自己的方法来打印一个列表,使用 lambdas 来管理是否插入了额外的空间,而不检查每次迭代。

#include <iostream>
#include <functional>

int main(int argc, char *argv[])
{
  auto printHead = [] (int num)
    {
      std::cout << num;
    };

  auto printTail = [] (int num)
    {
      std::cout << ' ' << num;
    };

  // Need explicit type for lambda if its going to be captured
  std::function<void(int)> print = [&printHead, &printTail, &print] (int num)
    {
      printHead(num);
      print = printTail;
    };

  for (auto& element : {1,2,3,4,5,6,6,7,8,9,6})
  {
    print(element);
  }

  return 0;
}

假设这个解决方案在性能方面比原始条件版本更有效,是否合理?

【问题讨论】:

  • 考虑到使用std::function 的开销,可能不会。这些天,分支预测非常好。话虽如此,I/O 时间将在这里绝对占主导地位,根本不值得担心这样的微优化。
  • 对。并且给定一个更大的列表,大到足以减轻std::function 的开销,它只会让分支预测更有理由相信该条件不适用于大多数情况。
  • @Yuushi 需要明确的是,唯一能阻止它有效工作的方法是std::function?如果我设法在不使用它的情况下做到这一点,它会起作用吗?
  • 可能。您有什么理由如此担心这里的开销吗?
  • 并非如此。我只是想知道分配 lambdas 是否会产生开销。我认为print = printTail 会产生额外的开销。

标签: c++ list loops c++11 lambda


【解决方案1】:

您可以使用 "Loop and a Half" 构造(我不确定我是否将指令计数与 @Cornstalks 相同,但 gcc -S -o - 提供 153 行汇编):

#include <iostream>
#include <vector>

int main()
{
    auto somelist = std::vector<int>{1,2,3,4,5,6,6,7,8,9,6};

    auto first = begin(somelist), last = end(somelist);
    if (first != last) {                // initial check
        while (true) {
            std::cout << *first++;     
            if (first == last) break;   // check in the middle
            std::cout << ", ";
        }
    }
}

Live Example 打印

1、2、3、4、5、6、6、7、8、9、6

即在最后一个元素的末尾没有分隔符(我使用“,”作为分隔符,因为它比空格更容易发现,当然可以根据您的意愿进行调整)。

“中间检查”是它与 range-for、whiledo-while 循环不同的原因。它还将一个 lambda 拆分为两个 std::for_each,这表明一个非常通用的版本将 std::for_each 推广到一个循环,该循环采用两个 lambda,一个在检查之前,一个在检查之后:

template<class It, class UnaryOp1, class UnaryOp2>
void loop_and_a_half(It first, It last, UnaryOp1 op1, UnaryOp2 op2)
{
    if (first == last) return;
    while (true) {
        op1(*first++);
        if (first == last) break;
        op2(*first);
    }    
}

可以这样调用(使用 C++14 通用 lambda):

loop_and_a_half(
    begin(somelist), end(somelist),
    [](auto e) { std::cout << e; },
    [](auto) { std::cout << ", "; }
);

Live Example 打印相同的输出。

【讨论】:

  • 不错,漂亮的替代品。仅供参考,我通过执行 otool -tv executable_name | wc -l 来估算指令数(根据您的代码,我的系统上大约有 228 条指令)。
  • 啊,不错,不知道那个工具。
【解决方案2】:

将条件放在循环之外怎么样?

if (!somelist.empty()) std::cout << somelist[0];
for (int i = 1; i < somelist.size(); ++i)
{
    std::cout << ' ' << somelist[i];
}

编辑:我没有回答实际问题。对比一下:

方法 1(~243 条指令):

std::vector<int> somelist = {1,2,3,4,5,6,6,7,8,9,6};
for (int idx{}; idx < somelist.size(); ++idx)
{
    if (idx == 0)
        std::cout << somelist[idx];

    else
        std::cout << ' ' << somelist[idx];
}

方法 2(~555 条指令):

auto printHead = [] (int num)
    {
        std::cout << num;
    };

auto printTail = [] (int num)
    {
        std::cout << ' ' << num;
    };

// Need explicit type for lambda if its going to be captured
std::function<void(int)> print = [&printHead, &printTail, &print] (int num)
    {
        printHead(num);
        print = printTail;
    };

std::vector<int> somelist = {1,2,3,4,5,6,6,7,8,9,6};
for (auto& element : somelist)
{
    print(element);
}

方法 3(~240 条指令):

std::vector<int> somelist = {1,2,3,4,5,6,6,7,8,9,6};

if (!somelist.empty()) std::cout << somelist[0];
for (int i = 1; i < somelist.size(); ++i)
{
    std::cout << ' ' << somelist[i];
}

这些都是在 OS X 上使用 clang++ 3.3 编译的,带有-Ofast -flto。虽然处理器是复杂的野兽,而且很难说哪个版本最快(在所有 CPU 中),但我认为方法 #3 可能是最快的(基于指令数),其次是关闭方法 #1,然后是方法#2。看起来您提出的方法阻止了很多编译器优化。

【讨论】:

    【解决方案3】:

    不涉及std::function 的更简单的解决方案怎么样?

    char space = '\0';
    for (auto& ele : somelist)
    {
      cout << space << ele;
      space = ' ';
    }
    

    或者你可以使用迭代器

    auto first = somelist.cbegin();
    auto last = somelist.cend();
    
    if(first != last) {
      cout << *first++;
    }
    for(; first != last; ++first) {
      cout << ' ' << *first;
    }
    

    std::function 使用类型擦除,很可能通过虚函数,将调用分派给目标函数;这将涉及一些开销。要确定它是否比其他示例更快/更慢,您只需对其进行测量。

    【讨论】:

    • 关于第二个实现,我会将first++ 移动到 for 循环的初始化器中,并将 for 循环移动到块中if 语句。
    【解决方案4】:

    为什么不对列表和列表元素都实现'

    • 列表的“
    • 每个元素的 '

    这会将您的原始代码简化为:

    cout << somelist;
    

    【讨论】:

    • 然而,在实现自定义 &lt;&lt; 运算符时,他仍然必须按照他在问题中所做的事情...
    • 对,但似乎 OP 当前代码的复杂之处在于处理打印最后一个元素的丑陋条件检查。我想,这可能可以通过 OOP 方法更清晰地抽象出来。
    猜你喜欢
    • 1970-01-01
    • 2012-03-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-12-31
    • 2021-12-23
    相关资源
    最近更新 更多