小编Bja*_*une的帖子

二进制堆的有效实现

我正在寻找有关如何有效实现二进制堆的信息.我觉得应该有一篇关于有效实现堆的好文章,但我还没找到.事实上,我已经无法找到任何有关超出基础知识的有效实现的资源,例如将堆存储在数组中.我正在寻找制作快速二进制堆的技术,超出我在下面描述的那些.

我已经编写了一个比Microsoft Visual C++和GCC的std :: priority_queue或使用std :: make_heap,std :: push_heap和std :: pop_heap更快的C++实现.以下是我在实现中已经涵盖的技术.我自己只想出了最后两个,尽管我怀疑这些是新想法:

(编辑:添加了关于内存优化的部分)

  • 从1开始索引
    查看维基百科的二进制堆实现说明.如果堆的根位于索引0处,则索引n处的节点的父,左子和右子的公式分别为(n-1)/ 2,2n + 1和2n + 2.如果使用基于1的数组,则公式变为更简单的n/2,2n和2n + 1.因此,使用基于1的数组时,父项和左子项更有效.如果p指向基于0的数组并且q = p-1,那么我们可以将p [0]作为q [1]来访问,因此使用基于1的数组没有开销.

  • 在使用leaf替换之前,将pop/remove移动元素设置到堆的底部
    经常通过将最顶部元素替换为最左边的底部叶子然后将其向下移动直到堆属性恢复来描述.这需要我们经历的每个级别的2次比较,并且由于我们将叶子移动到堆的顶部,因此我们可能会在堆中走得很远.所以我们应该期望比较少于2 log n.

    相反,我们可以在顶部元素所在的堆中留一个洞.然后我们通过迭代地移动较大的孩子来将那个洞向下移动.这需要我们经过的每个级别只有1个比较.通过这种方式,洞将成为一片叶子.此时,我们可以将最右下方的叶子移动到孔的位置,并将该值向上移动,直到堆属性恢复为止.由于我们移动的值是一片叶子,我们不希望它在树上移动很远.所以我们应该期待比log n更多的比较,这比以前更好.

  • 支持replace-top
    假设您要删除max元素并插入新元素.然后,您可以执行上述任何一个删除/弹出实现,但不是移动最右侧的底部叶子,而是使用您希望插入/推送的新值.(当大多数操作属于这种类型时,我发现锦标赛树比堆更好,但是堆更好一点.)

  • 使sizeof(T)的幂为2
    父,子和子子公式适用于索引,并且不能使它们直接在指针值上工作.所以我们将使用索引,这意味着从索引i中查找数组p中的值p [i].如果p是T*且i是整数,那么

    &(p[i]) == static_cast<char*>(p) + sizeof(T) * i
    
    Run Code Online (Sandbox Code Playgroud)

    并且编译器必须执行此计算以获得p [i].sizeof(T)是编译时常量,如果sizeof(T)是2的幂,则可以更有效地进行乘法.通过添加8个填充字节以将sizeof(T)从24增加到32,我的实现变得更快.降低缓存效率可能意味着这对于足够大的数据集来说并不是一个胜利.

  • 预乘法索引
    我的数据集性能提升了23%.除了查找父级,左子级和右子级之外,我们对索引执行的唯一操作是在数组中查找索引.因此,如果我们跟踪j = sizeof(T)*i而不是索引i,那么我们可以进行查找p [i]而不需要在评估p [i]时隐含的乘法,因为

    &(p[i]) == static_cast<char*>(p) + sizeof(T) * i == static_cast<char*>(p) + j
    
    Run Code Online (Sandbox Code Playgroud)

    然后,j值的左子和右子公式分别变为2*j和2*j + sizeof(T).父公式有点棘手,除了将j值转换为i值之外,我还没有找到方法做到这一点,如下所示:

    parentOnJ(j) = parent(j/sizeof(T))*sizeof(T) == (j/(2*sizeof(T))*sizeof(T)
    
    Run Code Online (Sandbox Code Playgroud)

    如果sizeof(T)是2的幂,那么这将编译为2个移位.这比使用索引i的普通父亲多1次操作.然而,我们然后在查找上保存1个操作.因此,净效应是找到父母用这种方式花费相同的时间,而左子和右子的查找变得更快. …

  • c++ performance computer-science priority-queue data-structures

    50
    推荐指数
    1
    解决办法
    9996
    查看次数

    标准库对自动分配的保证是什么?

    C++ 11标准对标准库相关的自移动赋值有何看法?更具体的是,什么(如果有的话)保证什么selfAssign呢?

    template<class T>
    std::vector<T> selfAssign(std::vector<T> v) {
      v = std::move(v);
      return v;
    }
    
    Run Code Online (Sandbox Code Playgroud)

    c++ stl move-semantics c++11

    41
    推荐指数
    2
    解决办法
    3488
    查看次数

    std :: atomic :: operator ++真的按值返回吗?

    根据这个前缀std::atomic<T>::operator++返回一个T,所以这段代码只增加v一次:

    template<class T> void addTwo(std::atomic<T>& v) {
      ++(++v);
    }
    
    Run Code Online (Sandbox Code Playgroud)

    此外,std::atomic<T>::operator= 显然返回a T,因此此代码取消引用用于指向临时的无效指针T:

    template<class T>
    void setOneThenTwo(std::atomic<T>& v) {
      auto ptr = &(v = 1);
      *ptr = 2;
    }
    
    Run Code Online (Sandbox Code Playgroud)

    我肯定不会建议这些代码模式是好的做法,但是对我来说std::atomic打破它们是非常令人惊讶的.我总是期望operator=和前缀operator++返回引用*this.

    问题:这里的返回类型是否正确cppreference,如果是这样,std::atomic在这方面是否有充分的理由表现出与内置类型不同的行为?

    c++ operator-overloading atomic c++-standard-library c++11

    13
    推荐指数
    1
    解决办法
    1258
    查看次数

    C++标准对堆栈溢出有什么看法?

    我看了一下C++ 0x标准草案,据我所知,堆栈溢出没有任何内容.搜索"堆栈溢出"不会产生任何结果,并且搜索"堆栈"我只获得了堆栈展开和std :: stack的引用.这是否意味着没有符合C++标准的实现,因为当本地对象(例如巨大的本地数组)耗尽内存时,没有机制允许处理错误?

    这个问题的答案表明,至少C标准没有提到堆栈溢出.

    要使问题具体,请考虑此计划

    // Program A
    int identity(int a) {
      if (a == 0)
        return 0;
      char hugeArray[1024 * 1024 * 1024]; // 1 GB
      return identity(a - 1) + 1;
    }
    int main() {
      return f(1024 * 1024 * 1024);
    }
    
    Run Code Online (Sandbox Code Playgroud)

    和这个程序

    // program B
    int main() {
      return 1024 * 1024 * 1024;
    }
    
    Run Code Online (Sandbox Code Playgroud)

    我认为C++标准不允许任何C++实现在这两个程序上做一些明显不同的事情.实际上程序A不会在任何现代机器上运行,因为它在堆栈上分配了一个exabyte内存(想象一下该函数实际上使用了巨大的数组,因此编译器无法以静默方式将其删除而不会产生不良影响).C++标准是否允许程序A失败?

    编辑:问题不在于标准是否应该定义堆栈溢出会发生什么,问题是它说什么,如果有的话.

    c++ stack-overflow undefined-behavior

    11
    推荐指数
    1
    解决办法
    387
    查看次数

    在没有密钥的情况下从哈希中查找unordered_map中的存储桶

    我正在使用std :: unordered_map.我有一个哈希值和一种方法来确定给定的候选键是否是我正在寻找的键,但我没有实际的键.我想查找与哈希值对应的存储桶,并浏览该存储桶中的每个元素,看看它是否是我要查找的元素.不幸的是,函数std :: unordered_map :: bucket(x)需要x作为键.没有先构建密钥,真的没有办法从哈希值中获取存储桶吗?

    您不需要回答问题的详细信息:我可以构建密钥,但在没有冲突的常见情况下,这将花费更长的时间,而不仅仅是检查我在桶中找到的单个候选是否是正确的.我的负载系数很低,因此冲突很少,即使是碰撞,完整的哈希值也不太可能匹配,因此很快就会确定不匹配.我关心这个因为我已经确定了一个分析器,关键构造花费了大量的时间 - 有很多查找,每次查找都需要构造一个密钥.

    更多细节,你真的不需要回答这个问题:键是整数的向量,我的查询是两个向量的总和.检查给定向量V是两个向量A和B的总和比将两个向量加到第三个向量C = A + B然后将C与V进行比较更快.我能够确定哈希值A + B没有计算实际矢量A + B,因为我存储了这些矢量的哈希值,而我的哈希函数f具有f(A + B)= f(A)+ f(B)的特性.所以我只需添加两个存储的哈希值来获得总和的哈希值.我已经确保保留一个备用向量,以便构造一个键不需要内存分配,但添加向量的代码仍然需要花费大量的时间.

    c++ stl c++11

    11
    推荐指数
    1
    解决办法
    1660
    查看次数

    在移动赋值和移动构造函数方面实现std :: swap

    以下是可能的定义std::swap:

    template<class T>
    void swap(T& a, T& b) {
      T tmp(std::move(a));
      a = std::move(b);
      b = std::move(tmp);
    }
    
    Run Code Online (Sandbox Code Playgroud)

    我相信

    1. std::swap(v,v) 保证没有效果
    2. std::swap 可以如上实现.

    以下引用似乎暗示这些信念是矛盾的.

    17.6.4.9函数参数[res.on.arguments]

    1除非另有明确说明,否则以下各项均适用于C++标准库中定义的函数的所有参数.

    ...

    • 如果函数参数绑定到右值引用参数,则实现可以假定此参数是对此参数的唯一引用.[注意:如果参数是T &&形式的通用参数并且绑定了类型A的左值,则参数绑定到左值引用(14.8.2.1),因此不包含在前一句中. - 结束注释] [注意:如果程序在将左值传递给库函数时将左值转换为x值(例如通过使用参数move(x)调用函数),程序实际上会要求该函数处理该左值作为临时的.实现可以自由地优化别名检查,如果参数是左值,则可能需要这些检查.-endnote]

    (感谢霍华德Hinnant(欣南特)用于提供报价)

    让我们v从标准模板库中获取一些可移动类型的对象并考虑该调用std::swap(v, v).在该行a = std::move(b);以上,这是内部的情况T::operator=(T&& t)的是this == &b,这样的参数是唯一的参考.这违反了上面的要求,因此该行在a = std::move(b)调用时调用未定义的行为std::swap(v, v).

    这里有什么解释?

    c++ stl move-semantics c++11

    10
    推荐指数
    2
    解决办法
    2030
    查看次数

    什么架构计算无效指针不安全?

    int* a = new int[5] - 1;
    
    Run Code Online (Sandbox Code Playgroud)

    这一行本身根据C++标准调用未定义的行为,因为a是一个无效的指针而不是一个接一个的结尾.同时,这是制作基于1的数组(第一个元素是[1])的零开销方式,我需要用于我的项目.

    我想知道这是否是我需要避免的,或者如果C++标准只是保守地支持一些奇怪的架构,我的代码永远都不会运行.所以问题是,这将是一个什么样的架构问题?这些广泛存在吗?

    编辑:要查看上面的行确实调用了未定义的行为,请查看此问题.

    编辑:Dennis Zickefoose指出,当调用未定义的行为时,允许编译器执行任何操作,因此编译器和CPU都必须提供超出C++标准的保证,以便像这样的代码工作.我正在将问题扩展到现代C++编译器是否存在这个问题.

    c++ cpu-architecture

    9
    推荐指数
    2
    解决办法
    579
    查看次数

    移动分配是通过destruct + move构造安全吗?

    这是使用移动构造函数为大多数类定义移动赋值的一种非常简单的方法:

    class Foo {
    public:
      Foo(Foo&& foo);                     // you still have to write this one
      Foo& operator=(Foo&& foo) {
        if (this != &foo) {               // avoid destructing the only copy
          this->~Foo();                   // call your own destructor
          new (this) Foo(std::move(foo)); // call move constructor via placement new
        }
        return *this;
      }
      // ...
    };
    
    Run Code Online (Sandbox Code Playgroud)

    这个调用你自己的析构函数的序列是否跟随在标准C++ 11中的this指针安全位置新增?

    c++ placement-new move-semantics c++11 move-assignment-operator

    8
    推荐指数
    1
    解决办法
    567
    查看次数

    具有相同散列的值是否在std :: unordered_map的同一个桶中?

    如果std :: unordered_map的两个键具有相同的哈希值,那么标准是否保证它们会进入同一个桶?我们假设根据模板等式谓词键不相等,它们只有相同的哈希值.

    额外问题:如果相同的哈希值并不意味着相同的存储桶,那么能够单独遍历存储桶的目的是什么?

    c++ stl c++11

    6
    推荐指数
    1
    解决办法
    544
    查看次数

    使用C++ 11克隆Cygwin

    我在Cygwin上安装了Clang,我尝试编译这段代码:

    #include <iostream>
    int main() {
      std::cout << "hello world!" << std::endl;
      return 0;
    }
    
    Run Code Online (Sandbox Code Playgroud)

    如果我这样做,那很好clang++ file.cpp.如果我这样做,它就不起作用clang++ file.cpp -std=c++11.我从标准标题中得到错误,如下所示:

    In file included from file.cpp:1:
    In file included from /usr/lib/gcc/i686-pc-cygwin/4.5.3/include/c++/iostream:39:
    In file included from /usr/lib/gcc/i686-pc-cygwin/4.5.3/include/c++/ostream:39:
    In file included from /usr/lib/gcc/i686-pc-cygwin/4.5.3/include/c++/ios:39:
    In file included from /usr/lib/gcc/i686-pc-cygwin/4.5.3/include/c++/exception:150:
    /usr/lib/gcc/i686-pc-cygwin/4.5.3/include/c++/exception_ptr.h:132:13: error:
          unknown type name 'type_info'
          const type_info*
    
    Run Code Online (Sandbox Code Playgroud)

    Cygwin Clang是不是开启了C++ 11,或者我可以做些什么来解决这个问题?

    c++ cygwin clang++

    6
    推荐指数
    2
    解决办法
    6654
    查看次数

    std :: atomic <int*> :: load应该进行比较和交换循环吗?

    简介:我曾预料到,只要加载的值很少改变std::atomic<int*>::load,std::memory_order_relaxed就会接近直接加载指针的性能.我看到原子负载的性能远远低于Visual Studio C++ 2012上的正常负载,因此我决定进行调查.事实证明原子负载是作为比较和交换循环实现的,我怀疑它不是最快的实现.

    问题:是否有某些原因std::atomic<int*>::load需要进行比较和交换循环?

    背景:我相信MSVC++ 2012正在基于此测试程序对指针的原子加载进行比较和交换循环:

    #include <atomic>
    #include <iostream>
    
    template<class T>
    __declspec(noinline) T loadRelaxed(const std::atomic<T>& t) {
      return t.load(std::memory_order_relaxed);
    }
    
    int main() {
      int i = 42;
      char c = 42;
      std::atomic<int*> ptr(&i);
      std::atomic<int> integer;
      std::atomic<char> character;
      std::cout
        << *loadRelaxed(ptr) << ' '
        << loadRelaxed(integer) << ' '
        << loadRelaxed(character) << std::endl;
      return 0;
    }
    
    Run Code Online (Sandbox Code Playgroud)

    我正在使用一个__declspec(noinline)函数来隔离与原子负载相关的汇编指令.我做了一个新的MSVC++ 2012项目,添加了一个x64平台,选择了发布配置,在调试器中运行程序并查看了反汇编.事实证明,两者std::atomic<char>std::atomic<int>参数最终会给出相同的调用loadRelaxed<int>- 这必须是优化器所做的事情.这是被调用的两个loadRelaxed实例的反汇编:

    loadRelaxed<int …

    c++ atomic visual-c++ compare-and-swap visual-studio-2012

    5
    推荐指数
    1
    解决办法
    1437
    查看次数