问题 在C或C ++中检查边界是否昂贵?


绑定检查很昂贵   (>运行时开销的x2倍)

我从我的一位教授那里得到了这一点。我很困惑。 据我所知,程序中最耗时的部分是IO(来自网络和硬盘)。

但是用C或C ++检查边界并不总是与那两个输入源相关。 例如,我使用C将一个buff的内容复制到另一个buff memcpy(dest, src, length(src))。在此之前,我检查大小 src 为了防止堆溢出。我可以成像的进动是:获取起始地址 src 和 \x00 字节 src(在汇编语言的视图中,我复制了内容 src 逐个查看字节是否等效 \x00)。获得2地址后,只需减去它们的长度即可 src。我读了内容 src 从记忆中。我们都知道从记忆中读取东西很快。


1282
2018-06-19 17:16


起源



答案:


我刚刚运行了一个程序,启用了迭代器边界检查。

运行时间从789毫秒增加到2608毫秒。

所以,是的,这很重要。不是所有的时间,但肯定比从来没有。

特别是,绑定检查的迭代器需要至少两倍于简单指针的存储空间,而且,不容易优化。从理论上讲,它们既简单又有效,但实际上你根本不想做你不需要的工作。

哦,我提到了  时间也从7.72秒增加到13.21秒?


对于你们中间的许多非信徒来说......一个微型的例子 没有边界检查的0.92秒 和 1.96秒


因为有很多人持怀疑态度 一切, 包含 vector效率......这是另一个:

#include <cstdio>
#include <ctime>

template<class T> struct Vector
{
    T *b, *e;
    Vector(size_t n) : b(new T[n]), e(b + n) { }
    T &operator[](size_t i) { return b[i]; }
    T &at(size_t i) { if (i >= e - b) { throw "invalid"; } return b[i]; }
};

#define at operator[]  // Comment this out to enable bounds-checking

int main(int argc, char **argv)
{
    Vector<size_t> v(1 << 16);
    for (size_t *p = v.b; p != v.e; ++p) { *p = 1; }
    clock_t begin = clock();
    for (int j = 0; j < 1 << 12; ++j)
    {
        for (size_t i = 8, n = v.e - v.b; i < n; ++i)
        {
            v.at(i) += v.at(i - 8);
            v.at(i) ^= v.at(i - 7);
            v.at(i) -= v.at(i - 6);
            v.at(i) ^= v.at(i - 5);
            v.at(i) += v.at(i - 4);
            v.at(i) ^= v.at(i - 3);
            v.at(i) -= v.at(i - 2);
            v.at(i) ^= v.at(i - 1);
        }
    }
    clock_t end = clock();
    fprintf(stderr, "%u\n", clock() - begin);
}

2.09秒 与 0.88秒


10
2018-06-19 17:27



在许多应用中,例如安全关键,无论额外的速度或空间如何,都需要进行边界检查。 - Thomas Matthews
@ThomasMatthews:是的,在那些应用程序中,你首先不会问这个问题。 - Mehrdad
我提到 为什么 它也使它变慢。具体来说,GCC 4.9用 -Ofast 展开未经检查的代码,但不是已检查的代码。通过 -fno-tree-loop-optimize 并且它们突然以相同的速度运行(由于一些额外的错过优化,它甚至比最初检查的代码慢得多)。 - Stuart Olsen
@StuartOlsen:有趣的是,这个特定的例子可能就是这种情况,但正如你可能已经意识到的那样,它只是一个错过特定优化的例子,而作为一般规则,即使没有特定/特定的优化,边界检查也会慢得多。我最初运行它的项目是 办法 比任何只从单一优化中获益的任何东西都要复杂得多;这个例子的唯一目的是一劳永逸地证明这个场景并不像人们似乎相信的那样牵强,事实上恰好出现在非常简单的程序中。 - Mehrdad
@Mehrdad你所展示的并不是那种绑定检查会造成2倍的减速;你已经表明它可以 避免 你正确指出的2倍加速是一个有限的循环子集。同样,最终结果是相同的 这个 特定代码,但不一定在没有特殊优化的情况下检查失败。在任何情况下,“可以”是有限用途的度量;一个人可以在有或没有绑定检查的情况下使代码任意变坏。绑定检查可能会使代码在常见情况下(即重要的代码)变慢,但这并未证明这一点。 - Stuart Olsen


答案:


我刚刚运行了一个程序,启用了迭代器边界检查。

运行时间从789毫秒增加到2608毫秒。

所以,是的,这很重要。不是所有的时间,但肯定比从来没有。

特别是,绑定检查的迭代器需要至少两倍于简单指针的存储空间,而且,不容易优化。从理论上讲,它们既简单又有效,但实际上你根本不想做你不需要的工作。

哦,我提到了  时间也从7.72秒增加到13.21秒?


对于你们中间的许多非信徒来说......一个微型的例子 没有边界检查的0.92秒 和 1.96秒


因为有很多人持怀疑态度 一切, 包含 vector效率......这是另一个:

#include <cstdio>
#include <ctime>

template<class T> struct Vector
{
    T *b, *e;
    Vector(size_t n) : b(new T[n]), e(b + n) { }
    T &operator[](size_t i) { return b[i]; }
    T &at(size_t i) { if (i >= e - b) { throw "invalid"; } return b[i]; }
};

#define at operator[]  // Comment this out to enable bounds-checking

int main(int argc, char **argv)
{
    Vector<size_t> v(1 << 16);
    for (size_t *p = v.b; p != v.e; ++p) { *p = 1; }
    clock_t begin = clock();
    for (int j = 0; j < 1 << 12; ++j)
    {
        for (size_t i = 8, n = v.e - v.b; i < n; ++i)
        {
            v.at(i) += v.at(i - 8);
            v.at(i) ^= v.at(i - 7);
            v.at(i) -= v.at(i - 6);
            v.at(i) ^= v.at(i - 5);
            v.at(i) += v.at(i - 4);
            v.at(i) ^= v.at(i - 3);
            v.at(i) -= v.at(i - 2);
            v.at(i) ^= v.at(i - 1);
        }
    }
    clock_t end = clock();
    fprintf(stderr, "%u\n", clock() - begin);
}

2.09秒 与 0.88秒


10
2018-06-19 17:27



在许多应用中,例如安全关键,无论额外的速度或空间如何,都需要进行边界检查。 - Thomas Matthews
@ThomasMatthews:是的,在那些应用程序中,你首先不会问这个问题。 - Mehrdad
我提到 为什么 它也使它变慢。具体来说,GCC 4.9用 -Ofast 展开未经检查的代码,但不是已检查的代码。通过 -fno-tree-loop-optimize 并且它们突然以相同的速度运行(由于一些额外的错过优化,它甚至比最初检查的代码慢得多)。 - Stuart Olsen
@StuartOlsen:有趣的是,这个特定的例子可能就是这种情况,但正如你可能已经意识到的那样,它只是一个错过特定优化的例子,而作为一般规则,即使没有特定/特定的优化,边界检查也会慢得多。我最初运行它的项目是 办法 比任何只从单一优化中获益的任何东西都要复杂得多;这个例子的唯一目的是一劳永逸地证明这个场景并不像人们似乎相信的那样牵强,事实上恰好出现在非常简单的程序中。 - Mehrdad
@Mehrdad你所展示的并不是那种绑定检查会造成2倍的减速;你已经表明它可以 避免 你正确指出的2倍加速是一个有限的循环子集。同样,最终结果是相同的 这个 特定代码,但不一定在没有特殊优化的情况下检查失败。在任何情况下,“可以”是有限用途的度量;一个人可以在有或没有绑定检查的情况下使代码任意变坏。绑定检查可能会使代码在常见情况下(即重要的代码)变慢,但这并未证明这一点。 - Stuart Olsen


这曾经是直到20世纪80年代才是真实的。

利用现代代码生成和高度流水线的CPU架构,可以在零或非常少的额外执行成本的情况下完成边界检查。这是因为边界检查可以与内存提取并行进行。


2
2018-06-19 17:27



不想在这里开始全面讨论,但“它比1980年更好”并不意味着什么。既不“不能改进”,也不“与整个程序相比没什么区别”。 - deviantfan
此外,“可以并行发生”并不一定意味着编译器总是可以成功地安排指令,使其“确实并行发生”......即使它可以很好地安排它们,事实是 array[n] = v 通常可以编译为单个指令而不进行边界检查,但可能需要半打或更多的边界检查,并且必须执行和退出这些附加指令的某些子集,这会导致运行时间的可测量增加,无论它们的哪些部分可以运行并行与否...... - twalberg
您是否认为读取数组的最大边界可能也涉及内存提取?一世 - Roddy
考虑到每年制造数十亿嵌入式处理器,通常使用精品编译器而不是高度流水线化的架构,在2014年检查C的界限仍然是一个很大的影响。有许多因素需要考虑。 - chux
@Roddy如果访问是在循环中,那么绑定很可能通过重命名缓存在寄存器中,至少在x86处理器的L1缓存中。 - Yichao Zhou