【问题标题】:elegant and fast operator function cpp优雅快速的算子函数 cpp
【发布时间】:2021-03-15 22:39:21
【问题描述】:

我目前正在开发一个程序,我的目标是使用尽可能少的存储空间尽可能快地在(一个类的)两个对象之间进行大量操作。

class number 
{
public:
    number(int x) :x{x} {};
    int x;

// option 1
    number operator+(number x)
    {
        return number(this->x + x.x);
    };

// option 2
    static void add(number* a, number* b, number* dest)
    {
        dest->x = a->x + b->x;
    };
};

int main()
{
    number a(2);
    number b(2);
    number c(0);

    // 4,608e-8 sec
    c = a + b;

    // 2,318e-8 sec
    number::add(&a,&b,&c);
}

我考虑过两种选择:

  1. 使用实际操作符
  2. 使用以三个变量(包括目标)作为参数的静态函数

第一个可读性最好,但大规模使用它可能意味着需要大量空间,因为每次运行都会初始化一个新对象。

我可能已经使用选项 2 解决了这个问题。通过获取指向目标的指针,从而重用存储空间。选项 2 读起来有点笨重,如果一个接一个地发生更多操作,代码可能很难理解。

我已经进行了几次速度和空间测试。 使用实际的算子函数每次运行槽需要 4,6e-8 秒和 920kb 存储。空隙占用 2,3e-8 和 915kb。

我缺少任何选项吗?如果不是,哪一个是存储空间、速度和可读性之间更好的权衡?

【问题讨论】:

  • 你能展示你用来做基准测试的代码吗?您还启用了优化吗?
  • 您说的是 0.00000002 秒的差异。几乎任何事情都可能导致这样的延迟。
  • 担心一个电话?真的吗?那是教科书式的过早优化,如果从设计的角度来看它是有意义的,就使用操作符。不,可能由于复制省略不会有任何新对象,如果有,那又如何? It's and int,你不用担心创建一个额外的 int。让编译器负责生成优化的代码。无论如何,你并没有测量这个程序的大小,有大量的包袱可以根据编译标志进入最终的 ELF。
  • 以您认为满足项目要求的最简单、最容易阅读和维护的方式编写代码。测试一下。是否符合要求?如果是,你就完成了。继续下一个问题或早点回家放松。如果它没有分析和隔离瓶颈。必要时使事情复杂化以改善一个瓶颈。测试。如果现在满足要求,则完成。如果没有分析、隔离和改进一个瓶颈。(或者如果您使最后一个更改变得更糟或没有将性能提高到足以值得增加的复杂性)。
  • 不清楚你想要达到什么。坦率地说,代码也无法演示它,因为要添加ints 你应该使用ints 和内置的+,你不会比这更快和​​内存效率更高

标签: c++ performance operator-overloading operators operator-keyword


【解决方案1】:

担心性能的“正确”方法是打开编译器优化。大多数时候就是这样。

编写正确且可读的代码。一旦你有了它,你就可以测量它。如果您发现一段代码很昂贵,您可以查看生成的程序集以了解为什么它很昂贵。

两个整数相加最简洁的方法是:

int main() {
    return 2+2;
}

Compiler output (gcc 9.2 with -O3):

main:
        mov     eax, 4
        ret

现在您可能希望将整数包装到一个类中:

struct number {
    int x;
};

int main() {
    return number{2}.x+number{2}.x;
}

这增加了创建类实例和访问其成员的成本。编译器输出:

main:
        mov     eax, 4
        ret

您可以使用operator+,而不是直接使用内置的+

struct number {
    int x;
    number operator+(const number& other){
        return {x+ other.x};
    }
};

int main() {
    return (number{2}+number{2}).x;
}

这增加了调用操作员的成本。编译器输出:

main:
        mov     eax, 4
        ret

您使用静态方法的版本(修改最少)是这样的:

struct number {
    int x;
    static void add(number* a, number* b, number* dest) {
        dest->x = a->x + b->x;
    };
};

int main()
{
    number a{2};
    number b{2};
    number c{0};
    number::add(&a,&b,&c);
    return c.x;
}

这增加了调用者必须“准备”返回值的成本,因为它是一个外参数。它通过指针增加了间接成本。它增加了使用使类膨胀的static 方法的成本。编译器输出:

main:
        mov     eax, 4
        ret

结论:不要进行过早的优化。在上述所有费用中,您支付的所有费用都在您的代码中,并且对生成的程序集没有影响。编写可读且简单的代码,并将 mirco 优化留给编译器。如果您编写了正确的代码并对其进行了测量,并且您意识到存在瓶颈,那么您当然可以尝试改进该部分。不过,在做任何这些之前,尝试改进您的number 以适应最一般的用例是徒劳的。编译器在这方面要好得多。

PS 关于你的数字(4,608e-8 秒 vs 2,318e-8 秒),我不得不承认我忽略了它们。 e-8 秒太少了,不重要。此外,对于基准测试,您需要共享基准测试的确切代码、您使用的编译器和哪些选项的详细信息,并说明您正在运行它的硬件。坦率地说,没有这些细节,这些数字毫无意义。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-02-21
    • 2017-05-10
    相关资源
    最近更新 更多