NULL 与 throw 和性能

发布于 2024-12-12 02:24:38 字数 395 浏览 0 评论 0原文

我有一个类:

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

get 调用getIfExists;如果元素存在,则返回该元素,如果不存在,则执行某些操作。 getIfExists 可以表明某些元素 i 不存在 要么抛出异常,要么返回 NULL。

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

I have a class:

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

get invokes getIfExists; if element exists, it is returned, if not, some action is performed. getIfExists can signal that some element i is not present
either by throwing exception, or by returning NULL.

Question: would there be any difference in performance? In one case, get will need to check ==NULL, in another try... catch.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(6

风流物 2024-12-19 02:24:38

这是设计问题,而不是性能问题。如果出现异常情况(例如在 get 函数中),则抛出异常;或者甚至更好地触发断言,因为违反函数前提条件是编程错误。如果它是预期的情况 - 就像您的 getIfExist 函数一样 - 那么不要抛出异常。

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

Its a matter of design, not performance. If its an exceptional situation -like in your get function- then throw an exception; or even better fire an assert since violation of a function precondition is a programming error. If its an expected case -like in your getIfExist function- then don't throw an exception.

Regarding performance, zero cost exception implementations exist (although not all compilers use that strategy). This means that the overhead is only paid when an exception its thrown, which should be... well... exceptionally.

痕至 2024-12-19 02:24:38

现代编译器实现“零成本”异常 - 它们仅在抛出时产生成本,并且成本与清理加上清理列表的缓存未命中成正比。因此,如果异常是异常的,它们确实可以比返回码更快。如果它们没有异常,它们可能会更慢。如果你的错误是在函数调用中的函数中的函数中,那么它实际上所做的工作要少得多。细节很有趣,非常值得谷歌搜索。

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

您应该编写最容易推理和维护的代码,然后分析它,并仅在出现瓶颈时重新审视您的决定。

Modern compilers implement 'zero cost' exceptions - they only incur cost when thrown, and the cost is proportional to the cleanup plus the cache-miss to get the list to clean up. Therefore, if exceptions are exceptional, they can indeed be faster than return-codes. And if they are unexceptional, they may be slower. And if your error is in a function in a function in a function call, it actually do much less work throwing. The details are fascinating and well worth googling.

But the cost is very marginal. In a tight loop it might make a difference, but generally not.

You should write the code that is easiest to reason about and maintain, and then profile it and revisit your decision only if its a bottleneck.

痴骨ら 2024-12-19 02:24:38

(请参阅评论!)

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

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

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

您认为哪种翻译方法更容易?

$ g++ -Os -c test.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 test.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>

(See comments!)

Without a doubt the return NULL variant has a better performance.

You should mostly never use exceptions when using return values is possible too.
Since the method is named get I assume NULL won't be a valid result value, so passing NULL should be the best solution.
If the caller does not test the result value, it dereferences a null value, rendering a SIGSEGV, what is appropriate too.

If the method is rarely called, you should not care about micro optimizations at all.

Which translated method looks easier to you?

$ g++ -Os -c test.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 test.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>
青丝拂面 2024-12-19 02:24:38

性能肯定会有差异(如果你给 Vector::getIfExists 一个 throw() 规范,甚至可能会有很大的差异,但我在这里推测一下)。但在我看来,这是只见树木不见森林。

金钱问题是:您是否要使用越界参数多次调用此方法?如果是,为什么?

There will certainly be a difference in performance (maybe even a very big one if you give Vector::getIfExists a throw() specification, but I 'm speculating a bit here). But IMO that's missing the forest for the trees.

The money question is: are you going to call this method so many times with an out-of-bounds parameter? And if yes, why?

蓝海 2024-12-19 02:24:38

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

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

Yes, there would be a difference in performance: returning NULL is less expensive than throwing an exception, and checking for NULL is less expensive than catching an exception.

Addendum: But performance is only relevant if you expect that this case will happen frequently, in which case it's probably not an exceptional case anyway. In C++, it's considered bad style to use exceptions to implement normal program logic, which this seems to be: I'm assuming that the point of get is to auto-extend the vector when necessary?

清醇 2024-12-19 02:24:38

如果调用者希望处理某个项目不存在的可能性,您应该以一种表明这一点的方式返回,而不抛出异常。如果调用者没有做好准备,您应该抛出异常。当然,被调用的例程不可能神奇地知道调用者是否准备好迎接麻烦。需要考虑的几种方法:

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

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

If the caller is going to be expecting to deal with the possibility of an item not existing, you should return in a way that indicates that without throwing an exception. If the caller is not going to be prepared, you should throw an exception. Of course, the called routine isn't likely to magically know whether the caller is prepared for trouble. A few approaches to consider:

  1. Microsoft's pattern is to have a Get() method, which returns the object if it exists and throws an exception if it doesn't, and a TryGet() method, which returns a Boolean indicating whether the object existed, and stores the object (if it exists) to a Ref parameter. My big complaint with this pattern is that interfaces using it cannot be covariant.
  2. A variation, which I often prefer for collections of reference types, is to have Get and TryGet methods, and have TryGet return null for non-existent items. Interface covariance works much better this way.
  3. A slight variation on the above, which works even for value-types or unconstrained generics, is to have the TryGet method accept a Boolean by reference, and store to that Boolean a success/fail indicator. In case of failure, the code can return an unspecified object of the appropriate type (most likely default<T>.
  4. Another approach, which is particularly suitable for private methods, is to pass either a Boolean or an enumerated type specifying whether a routine should return null or throw an exception in case of failure. This approach can improve the quality of generated exceptions while minimizing duplicated code. For example, if one is trying to get a packet of data from a communications pipe and the caller isn't prepared for failure, and an error occurs in a routine that reads the packet header, an exception should probably be thrown by the packet-header-read routine. If, however, the caller will be prepared not to receive a packet, the packet-header-read routine should indicate failure without throwing. The cleanest way to allow for both possibilities would be for the read-packet routine to pass an "errors will be dealt with by caller" flag to the read-packet-header routine.
  5. In some contexts, it may be useful for a routine's caller to pass a delegate to be invoked in case anticipated problems arise. The delegate could attempt to resolve the problem, and do something to indicate whether the operation should be retried, the caller should return with an error code, an exception should be raised, or something else entirely should happen. This can sometimes be the best approach, but it's hard to figure out what data should be passed to the error delegate and how it should be given control over the error handling.

In practice, I tend to use #2 a lot. I dislike #1, since I feel that the return value of a function should correspond with its primary purpose.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文