当前位置: 首页 > 面试题库 >

与类似的内存访问相比,C ++中的链表迭代比Go中的慢

卓致远
2023-03-14
问题内容

在各种情况下,我观​​察到C
++中的链表迭代始终比Go中的慢10-15%。我在解决堆栈溢出这个神秘的第一次尝试是在这里。我编写的示例有问题,因为:

1)由于堆分配,内存访问是不可预测的,并且

2)因为没有实际的工作要做,所以一些人的编译器正在优化主循环。

为了解决这些问题,我有一个新程序,其中包含C 和Go的实现。C

版本花费1.75秒,而Go版本花费1.48秒。这次,我在计时开始之前进行了一次大堆分配,并使用它来操作对象池,并从中释放并获取链表的节点。这样,两种实现之间的内存访问应该完全相似。

希望这可以使奥秘重现!

C ++:

#include <iostream>
#include <sstream>
#include <fstream>
#include <string>
#include <vector>
#include <boost/timer.hpp>

using namespace std;

struct Node {
    Node *next; // 8 bytes
    int age;   // 4 bytes
};

// Object pool, where every free slot points to the previous free slot
template<typename T, int n>
struct ObjPool
{
    typedef T*       pointer;
    typedef pointer* metapointer;

    ObjPool() :
        _top(NULL),
        _size(0)
    {
        pointer chunks = new T[n];
        for (int i=0; i < n; i++) {
            release(&chunks[i]);
        }
    }

    // Giver an available pointer to the object pool
    void release(pointer ptr)
    {
        // Store the current pointer at the given address
        *(reinterpret_cast<metapointer>(ptr)) = _top;

        // Advance the pointer
        _top = ptr;

        // Increment the size
        ++_size;
    }

    // Pop an available pointer off the object pool for program use
    pointer acquire(void)
    {
        if(_size == 0){throw std::out_of_range("");}

        // Pop the top of the stack
        pointer retval = _top;

        // Step back to the previous address
        _top = *(reinterpret_cast<metapointer>(_top));

        // Decrement the size
        --_size;

        // Return the next free address
        return retval;
    }

    unsigned int size(void) const {return _size;}

protected:
    pointer _top;

    // Number of free slots available
    unsigned int _size;
};

Node *nodes = nullptr;
ObjPool<Node, 1000> p;

void processAge(int age) {
    // If the object pool is full, pop off the head of the linked list and release
    // it from the pool
    if (p.size() == 0) {
        Node *head = nodes;
        nodes = nodes->next;
        p.release(head);
    }

    // Insert the new Node with given age in global linked list. The linked list is sorted by age, so this requires iterating through the nodes.
    Node *node = nodes;
    Node *prev = nullptr;
    while (true) {
        if (node == nullptr || age < node->age) {
            Node *newNode = p.acquire();
            newNode->age = age;
            newNode->next = node;

            if (prev == nullptr) {
                nodes = newNode;
            } else {
                prev->next = newNode;
            }

            return;
        }

        prev = node;
        node = node->next;
    }
}

int main() {
    Node x = {};
    std::cout << "Size of struct: " << sizeof(x) << "\n"; // 16 bytes

    boost::timer t;
    for (int i=0; i<1000000; i++) {
        processAge(i);
    }

    std::cout << t.elapsed() << "\n";
}

走:

package main

import (
    "time"
    "fmt"
    "unsafe"
)

type Node struct {
    next *Node // 8 bytes
    age int32 // 4 bytes
}

// Every free slot points to the previous free slot
type NodePool struct {
    top *Node
    size int
}

func NewPool(n int) NodePool {
    p := NodePool{nil, 0}
    slots := make([]Node, n, n)
    for i := 0; i < n; i++ {
        p.Release(&slots[i])
    }

    return p
}

func (p *NodePool) Release(l *Node) {
    // Store the current top at the given address
    *((**Node)(unsafe.Pointer(l))) = p.top
    p.top = l
    p.size++
}

func (p *NodePool) Acquire() *Node {
    if p.size == 0 {
        fmt.Printf("Attempting to pop from empty pool!\n")
    }
    retval := p.top

    // Step back to the previous address in stack of addresses
    p.top = *((**Node)(unsafe.Pointer(p.top)))
    p.size--
    return retval
}

func processAge(age int32) {
    // If the object pool is full, pop off the head of the linked list and release
    // it from the pool
    if p.size == 0 {
        head := nodes
        nodes = nodes.next
        p.Release(head)
    }

    // Insert the new Node with given age in global linked list. The linked list is sorted by age, so this requires iterating through the nodes.
    node := nodes
    var prev *Node = nil
    for true {
        if node == nil || age < node.age {
            newNode := p.Acquire()
            newNode.age = age
            newNode.next = node

            if prev == nil {
                nodes = newNode
            } else {
                prev.next = newNode
            }
            return
        }

        prev = node
        node = node.next
    }
}

// Linked list of nodes, in ascending order by age
var nodes *Node = nil
var p NodePool = NewPool(1000)

func main() {
    x := Node{};
    fmt.Printf("Size of struct: %d\n", unsafe.Sizeof(x)) // 16 bytes

    start := time.Now()
    for i := 0; i < 1000000; i++ {
        processAge(int32(i))
    }

    fmt.Printf("Time elapsed: %s\n", time.Since(start))
}

输出:

clang++ -std=c++11 -stdlib=libc++ minimalPool.cpp -O3; ./a.out
Size of struct: 16
1.7548

go run minimalPool.go
Size of struct: 16
Time elapsed: 1.487930629s

问题答案:

这两个程序之间的最大区别是,Go代码会忽略错误(如果幸运的话,如果您清空池,则会忽略错误(如果发生错误,将会恐慌或出现段错误),而C
++代码则会通过异常传播错误。比较:

if p.size == 0 {
    fmt.Printf("Attempting to pop from empty pool!\n")
}

if(_size == 0){throw std::out_of_range("");}

至少有三种方法1可以使比较公平:

  1. 可以像在Go中一样更改C ++代码以忽略该错误,
  2. 将两个版本都更改为panic/ abort错误。
  3. 像在C ++中一样,更改Go版本以惯用的方式处理错误2。

因此,让我们全部完成并比较结果3:

  • C ++忽略错误:墙1.059329s,用户1.050000s + 0.000000s系统= 1.050000s html" target="_blank">CPU(99.1%)
  • C ++因错误而中止:1.081585s墙,1.060000s用户+ 0.000000s系统= 1.060000s CPU(98.0%)
  • 对错误惊慌:经过的时间:1.152942427s
  • 忽略错误:经过的时间:1.196426068s
  • 进行惯常的错误处理:经过的时间:1.322005119s
  • C ++例外:壁1.373458s,用户1.360000s + 0.000000s系统= 1.360000s CPU(99.0%)

所以:

  • 没有错误处理,C ++比Go更快。
  • 有了恐慌,Go的速度提高了4倍,但仍然不及C ++。
  • 通过惯用的错误处理,C ++的速度比Go慢得多。

为什么?该异常实际上不会在您的测试运行中发生,因此实际的错误处理代码绝不会以任何一种语言运行。但是clang无法证明它不会发生。而且,由于您永远不会catch在任何地方出现异常,这意味着它必须针对整个堆栈中的每个非遗漏帧发出异常处理程序和堆栈展开器。因此,它在每个函数调用和返回上都要做更多的工作-
不需要 更多的工作,但是随后您的函数做的实际工作却很少,以至于不必要的额外工作加起来了。

1.您还可以更改C ++版本以执行C风格的错误处理,或使用Option类型,以及可能的其他可能性。

2.当然,这需要进行很多更改:您需要导入errors,将的返回类型更改Acquire(*Node, error),将的返回类型更改processAgeerror,更改您的所有return语句并至少添加两个if err != nil { … }检查。但这对Go来说应该是一件好事,对吧?

3.当我在这,我换成你的遗产boost::timerboost::auto_cpu_timer,所以我们现在看到的挂钟时间(如围棋)以及CPU时间。

4.我不会尝试解释原因,因为我不理解。快速浏览一下组装,显然已经对某些检查进行了优化,但是我不明白为什么如果没有,就无法优化相同的检查panic



 类似资料:
  • 问题内容: 编辑:获得一些反馈后,我创建了一个新示例,该示例应更具可重复性。 我一直在用C 编写一个项目,其中涉及许多链表迭代。为了获得基准,我重写了Go中的代码。令人惊讶的是,我发现即使将-O标志传递给clang ,Go实现的运行速度也始终稳定〜10%。可能我只是缺少一些C ++的明显优化,但是我已经通过各种调整将自己的头撞墙了一段时间了。 这是一个简化的版本,在C 和Go中具有相同的实现,其中

  • 问题内容: 我想知道与ArrayList相比,Java HashMap的内存开销是多少? 更新: 我想提高搜索相同对象的大包装(600万以上)的特定值的速度。 因此,我正在考虑使用一个或多个HashMap而不是使用ArrayList。但是我想知道HashMap的开销是多少。 据我了解,密钥不是存储的,只是密钥的哈希,因此它应该类似于 对象的哈希大小+一个指针 。 但是使用什么哈希函数?是对象提供的

  • 问题内容: 我想将多个字符串相互比较,并找到最相似的字符串。我想知道是否有任何库,方法或最佳实践会返回我哪些字符串与其他字符串更相似的字符串。例如: “The quick fox jumped” -> “The fox jumped” “The quick fox jumped” -> “The fox” 该比较将返回第一个比第二个更相似。 我想我需要一些方法,例如: 某处有这样的东西吗? 编辑:

  • 我在读关于CRCs,我偶然发现了CRC目录和这篇关于CRC-CCITT的文章。 我基于第二个链接实现了(参见下面的代码)。 我是不是遗漏了异或运算的一些属性?在我看来,这两个算法应该有相同的输出(当然不考虑第一个算法的增强),但它们没有。 PS:可执行代码:http://ideone.com/mkuqqq

  • 问题内容: 我只是想知道在类实例中是否建议使用getter方法访问类变量,以及直接访问是否存在明显的性能差异。特别是在预期在jvm中生成许多对象的情况下。 问题答案: 在Java中,惯例是通过类外部的getter / setter访问所有字段。通常,从类内部直接访问字段。但是,您也可以根据需要通过getter / setter访问它们。 重要的是要知道这只是一个约定。许多其他编程语言没有如此严格的

  • 问题内容: 我想让’==’运算符在我的程序中使用近似比较:如果x和y的浮点值相等(==) 有什么好的方法呢?鉴于float是内置类型,我认为我不能重新定义==运算符,可以吗? 请注意,我想使用float的其他功能,唯一要更改的是相等运算符。 编辑: 感谢您的回答,我了解您关于可读性和其他问题的观点。 也就是说,如果可能的话,我真的更希望实际上继续使用常规的float类型,而不是使用新的类或新的比较