NULL与投掷和性能

NULL vs throw and performance

本文关键字:性能 NULL      更新时间:2023-10-16

我有一个类:

class Vector {
    public:
    element* get(int i);
    private:
    element* getIfExists(int i):
};

CCD_ 1调用CCD_;如果元素存在,则返回该元素;如果不存在,则执行某些操作。CCD_ 3可以用信号表示某个元件CCD_要么抛出异常,要么返回NULL。

问题:在表现上会有什么不同吗?在一种情况下,get将需要检查==NULL,在另一种情况中,try... catch

这是设计问题,而不是性能问题。如果这是一种特殊情况,比如在get函数中,那么抛出一个异常;或者更好地激发断言,因为违反函数前提条件是编程错误。如果这是一个预期的情况——比如在getIfExist函数中——那么不要抛出异常。

关于性能,存在零成本异常实现(尽管并非所有编译器都使用该策略(。这意味着只有在抛出异常时才支付开销,该异常应该是…嗯。。。破例

现代编译器实现"零成本"异常-它们只在抛出时产生成本,成本与清理加上缓存未命中成比例,以获得要清理的列表。因此,如果异常是异常的,那么它们确实可以比返回代码更快。如果它们是普通的,它们可能会更慢。如果你的错误发生在函数调用中的函数中的一个函数中,那么它实际上会减少抛出的工作量。细节引人入胜,值得在谷歌上搜索。

但成本是非常微不足道的。在一个紧密的循环中,它可能会产生影响,但通常不会。

您应该编写最容易推理和维护的代码,然后配置它,只有在它是瓶颈时才重新考虑您的决定。

(见注释!(

毫无疑问,get0变体具有更好的性能。

当使用返回值也是可能的时,您基本上不应该使用异常。由于该方法名为get,我认为NULL不是有效的结果值,因此传递NULL应该是最好的解决方案。如果调用者没有测试结果值,它会取消引用一个null值,从而呈现一个SIGSEGV,这也是合适的。

如果很少调用该方法,则根本不应该关心微观优化。

哪种翻译方法对你来说更容易?

$g++-Os-c测试.cpp

#include <cstddef>
void *get_null(int i) throw ();
void *get_throwing(int i) throw (void*);
int one(int i) {
    void *res = get_null(i);
    if(res != NULL) {
        return 1;
    }
    return 0;
}
int two(int i) {
    try {
        void *res = get_throwing(i);
        return 1;
    } catch(void *ex) {
        return 0;
    }
}

$objdump-dC测试.o

0000000000000000 <one(int)>:
   0:   50                      push   %rax
   1:   e8 00 00 00 00          callq  6 <one(int)+0x6>
   6:   48 85 c0                test   %rax,%rax
   9:   0f 95 c0                setne  %al
   c:   0f b6 c0                movzbl %al,%eax
   f:   5a                      pop    %rdx
  10:   c3                      retq   
0000000000000011 <two(int)>:
  11:   56                      push   %rsi
  12:   e8 00 00 00 00          callq  17 <two(int)+0x6>
  17:   b8 01 00 00 00          mov    $0x1,%eax
  1c:   59                      pop    %rcx
  1d:   c3                      retq   
  1e:   48 ff ca                dec    %rdx
  21:   48 89 c7                mov    %rax,%rdi
  24:   74 05                   je     2b <two(int)+0x1a>
  26:   e8 00 00 00 00          callq  2b <two(int)+0x1a>
  2b:   e8 00 00 00 00          callq  30 <two(int)+0x1f>
  30:   e8 00 00 00 00          callq  35 <two(int)+0x24>
  35:   31 c0                   xor    %eax,%eax
  37:   eb e3                   jmp    1c <two(int)+0xb>

性能肯定会有差异(如果给Vector::getIfExists一个throw()规范,甚至可能会有很大的差异,但我在这里有点猜测(。但国际海事组织认为,这是为了树木而忽视森林。

钱的问题是:你会用越界参数多次调用这个方法吗?如果是,为什么

是的,性能会有所不同:返回NULL比抛出异常便宜,检查NULL比捕获异常便宜。

附录:但只有当您预计这种情况会频繁发生时,性能才是相关的,在这种情况下,它可能不是例外情况。在C++中,使用异常来实现正常的程序逻辑被认为是一种糟糕的风格,这似乎是:我假设get的重点是在必要时自动扩展向量?

如果调用方希望处理某个项不存在的可能性,则应以指示的方式返回,而不引发异常。如果调用者没有做好准备,那么应该抛出一个异常。当然,被调用的例程不太可能神奇地知道调用者是否做好了应对麻烦的准备。需要考虑的几种方法:

  1. 微软的模式是有一个Get((方法和一个TryGet((方法,前者在对象存在时返回对象,如果不存在则抛出异常,后者返回指示对象是否存在的布尔值,并将对象(如果存在(存储到Ref参数中。我对这种模式的最大抱怨是,使用它的接口不能是协变的。
  2. 对于引用类型的集合,我通常更喜欢的一种变体是使用Get和TryGet方法,并让TryGet为不存在的项返回null。接口协方差通过这种方式工作得更好。
  3. 上面的一个细微变化是,TryGet方法通过引用接受布尔值,并将成功/失败指示符存储到该布尔值中,这甚至适用于值类型或无约束泛型。在失败的情况下,代码可以返回适当类型的未指定对象(很可能是默认的<T>。
  4. 另一种特别适用于私有方法的方法是传递布尔类型或枚举类型,指定例程在失败时是返回null还是抛出异常。这种方法可以提高生成异常的质量,同时最大限度地减少重复代码。例如,如果试图从通信管道获取数据包,而调用者没有为失败做好准备,并且在读取数据包标头的例程中发生错误,则数据包标头读取例程可能会引发异常。但是,如果调用方准备不接收数据包,则数据包标头读取例程应指示失败而不抛出。允许这两种可能性的最干净的方法是读数据包例程将"错误将由调用者处理"标志传递给读数据包头例程。
  5. 在某些情况下,例程的调用程序传递一个委托以供在出现预期问题时调用可能很有用。委托可以尝试解决问题,并做一些事情来指示是否应该重试操作,调用方是否应该返回错误代码,是否应该引发异常,或者是否应该完全发生其他事情。这有时可能是最好的方法,但很难弄清楚哪些数据应该传递给错误委托,以及如何让它控制错误处理。

在实践中,我倾向于经常使用#2。我不喜欢#1,因为我觉得函数的返回值应该与其主要目的相对应。