【问题标题】:The fastest way to use a binary expression on array of booleans在布尔数组上使用二进制表达式的最快方法
【发布时间】:2015-10-04 21:52:49
【问题描述】:

我需要以最快的方式做这样的事情(O(1) 是完美的):

for (int j = 0; j < V; ++j)
        {
            if(!visited[j]) required[j]=0;
        }

我想出了这个解决方案:

for (int j = 0; j < V; ++j)
        {
             required[j]=visited[j]&required[j];
        }

这使程序的运行速度提高了 3 倍,但我相信还有更好的方法可以做到这一点。我说的对吗?

顺便说一句。 required 和visited 是动态分配的数组

bool *required;
bool *visited;
required = new bool[V];
visited = new bool[V];

【问题讨论】:

  • 我怀疑你可以在O(1) 中做到这一点。还要确保你正在编译优化。
  • 您可以在 O(1) 中完全不这样做。在实际需要时懒惰地计算 required[j] 的值。
  • “最快的方式”与大 O 和诸如此类的关系不大,更多的是与您已经在做的低级优化有关。你想要更多吗?也许使用 SIMD 内在函数?
  • 你必须使用布尔数组吗? bitset(或dynamic_bitset)之类的东西会快得多,因为一条指令可以同时处理 32 或 64 个布尔值。
  • 您接受答案了吗? @Pawelnr1

标签: c++ arrays optimization boolean-operations


【解决方案1】:

在您使用简单对象列表的情况下,您很可能最适合使用 C++ 标准库提供的功能。所有现代编译器都可以非常有效地识别和优化 valarray 和向量等结构。

关于您可以在多大程度上依赖您的编译器存在很多争论,但一个保证是,您的编译器是与标准库一起构建的,并且依赖它来实现基本功能(例如您的问题)通常是一个安全的选择。

永远不要害怕运行您自己的时间测试并与您的编译器竞争!这是一项有趣的练习,而且越来越难以实现。

构造一个 valarray(在 c++11 及更高版本中高度优化):

std::valarray<bool> valRequired(required, V);
std::valarray<bool> valVisited(visited, V);
valRequired &= valVisited;

或者,您可以使用转换用一行来完成:

std::transform(required[0], required[V-1], visited[0], required[0], [](bool r, bool v){ return r & v; })

编辑:虽然更少的行并不更快,但您的编译器可能会向量化此操作。

我也测试了他们的时间:

int main(int argc, const char * argv[]) {
    auto clock = std::chrono::high_resolution_clock{};
    {
        bool visited[5] = {1,0,1,0,0};
        bool required[5] = {1,1,1,0,1};

        auto start = clock.now();
        for (int i = 0; i < 5; ++i) {
            required[i] &= visited[i];
        }
        auto end = clock.now();
        std::cout << "1: " << (end - start).count() << std::endl;
    }

    {
        bool visited[5] = {1,0,1,0,0};
        bool required[5] = {1,1,1,0,1};

        auto start = clock.now();
        for (int i = 0; i < 5; ++i) {
            required[i] = visited[i] & required[i];
        }
        auto end = clock.now();
        std::cout << "2: " << (end - start).count() << std::endl;
    }

    {
        bool visited[5] = {1,0,1,0,0};
        bool required[5] = {1,1,1,0,1};

        auto start = clock.now();
        std::transform(required, required + 4, visited, required, [](bool r, bool v){ return r & v; });
        auto end = clock.now();
        std::cout << "3: " << (end - start).count() << std::endl;
    }

    {
        bool visited[5] = {1,0,1,0,0};
        bool required[5] = {1,1,1,0,1};
        std::valarray<bool> valVisited(visited, 5);
        std::valarray<bool> valrequired(required, 5);

        auto start = clock.now();
        valrequired &= valVisited;
        auto end = clock.now();
        std::cout << "4: " << (end - start).count() << std::endl;
    }
}

输出:

1: 102
2: 55
3: 47
4: 45
Program ended with exit code: 0

【讨论】:

  • @RussSchultz 和 harold 你们都是对的,这当然不是魔法。事实上,编译器通过它是一个标准,对标准库有非常非神奇的洞察力。使用它可以提供一致性并保证实现对您的编译器来说是可访问和熟悉的;不像你在 90 年代试图优化你的代码。是的,您都说得对,在某些情况下,可以预见编译器会采用不熟悉的对象结构的“漫长路线”,并且应该适当地处理这些情况。
  • 正如@harold 指出的那样,这不是其中一种情况。但是,我会在我的回答中修改并注意这一点。如果您发现我的回答还有改进的余地,请告诉我。
  • 不,编译器只能洞察 STL 的接口,而不是实现(因为 STL 可以被替换,即 newlib 与 uclib,与供应商提供的)。编译器不能(或不应该)对 STL 调用内部发生的情况做出任何假设,尽管我知道一些嵌入式产品的编译器将 strcpy、strlen、memcpy、memset 等转换为内联汇编而不是调用进入 libc。
  • 话虽如此,首先使用 STL,然后在代码工作后寻找优化。没有什么比为每个操作都尝试正确的最佳解决方案而陷入困境更糟糕的了。
  • @RussSchultz 有趣的是,我的理解是编译器提供了自己的 STL 实现?在阅读了几篇文章后,我发现它是与它们捆绑在一起的 ABI。感谢您的洞察力!
【解决方案2】:

在@AlanStokes 的行中,使用打包的二进制数据并结合AVX 指令_mm512_and_epi64,一次512 位。做好头发乱糟糟的准备。

【讨论】:

    猜你喜欢
    • 2014-04-21
    • 2016-08-18
    • 2017-10-02
    • 2018-05-29
    • 2021-05-06
    • 1970-01-01
    • 1970-01-01
    • 2011-01-03
    • 1970-01-01
    相关资源
    最近更新 更多