La iteración sobre la lista vinculada en C ++ es más lenta que en Go

EDIT: después de recibir algunos comentarios, creé unnuevo ejemplo que debería ser más reproducible.

He estado escribiendo un proyecto en C ++ que involucra muchas iteraciones de listas vinculadas. Para obtener un punto de referencia, reescribí el código en Go. Sorprendentemente, descubrí que la implementación de Go se ejecuta constantemente más rápido en ~ 10%, incluso después de pasar el indicador -O a clang ++. Probablemente solo me falta alguna optimización obvia en C ++, pero he estado golpeándome la cabeza contra la pared por un tiempo con varios ajustes.

Aquí hay una versión simplificada, con implementaciones idénticas en C ++ y Go, donde el programa Go se ejecuta más rápido. Todo lo que hace es crear una lista vinculada con 3000 nodos, y luego calcular el tiempo que tarda en iterar sobre esta lista 1,000,000 de veces (7.5 segundos en C ++, 6.8 en Go).

C ++:

#include <iostream>
#include <chrono>

using namespace std;
using ms = chrono::milliseconds;

struct Node {
    Node *next;
    double age;
};

// Global linked list of nodes
Node *nodes = nullptr;

void iterateAndPlace(double age) {
    Node *node = nodes;
    Node *prev = nullptr;

    while (node != nullptr) {
        // Just to make sure that age field is accessed
        if (node->age > 99999) {
            break;
        }

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

    // Arbitrary action to make sure the compiler
    // doesn't optimize away this function
    prev->age = age;
}

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

    // Fill in global linked list with 3000 dummy nodes
    for (int i=0; i<3000; i++) {
        Node* newNode = new Node;
        newNode->age = 0.0;
        newNode->next = nodes;
        nodes = newNode;
    }

    auto start = chrono::steady_clock::now();
    for (int i=0; i<1000000; i++) {
        iterateAndPlace(100.1);
    }

    auto end = chrono::steady_clock::now();
    auto diff = end - start;
    std::cout << "Elapsed time is :  "<< chrono::duration_cast<ms>(diff).count()<<" ms "<<endl;
}

Ir

package main
import (
    "time"
    "fmt"
    "unsafe"
)

type Node struct {
    next *Node
    age float64
}

var nodes *Node = nil

func iterateAndPlace(age float64) {
    node := nodes
    var prev *Node = nil

    for node != nil {
        if node.age > 99999 {
            break
        }
        prev = node
        node = node.next
    }

    prev.age = age
}

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

    for i := 0; i < 3000; i++ {
        newNode := new(Node)
        newNode.next = nodes
        nodes = newNode
    }

    start := time.Now()
    for i := 0; i < 1000000; i++ {
        iterateAndPlace(100.1)
    }
    fmt.Printf("Time elapsed: %s\n", time.Since(start))
}

Salida de mi Mac:

$ go run minimal.go
Size of struct: 16
Time elapsed: 6.865176895s

$ clang++ -std=c++11 -stdlib=libc++ minimal.cpp -O3; ./a.out
Size of struct: 16
Elapsed time is :  7524 ms

Clang versión:

$ clang++ --version
Apple LLVM version 8.0.0 (clang-800.0.42.1)
Target: x86_64-apple-darwin15.6.0
Thread model: posix

EDIT: UKMonkey mencionó el hecho de que los nodos pueden asignarse de forma contigua en Go pero no en C ++. Para probar esto, asigné contiguamente en C ++ con un vector, y esto no cambió el tiempo de ejecución:

// Fill in global linked list with 3000 contiguous dummy nodes
vector<Node> vec;
vec.reserve(3000);
for (int i=0; i<3000; i++) {
    vec.push_back(Node());
}

nodes = &vec[0];
Node *curr = &vec[0];
for (int i=1; i<3000; i++) {
    curr->next = &vec[i];
    curr = curr->next;
    curr->age = 0.0;
}

Compruebo que la lista vinculada resultante es contigua:

std::cout << &nodes << " " << &nodes->next << " " << &nodes->next->next << " " << &nodes->next->next->next << "\n";
0x1032de0e0 0x7fb934001000 0x7fb934001010 0x7fb934001020

Respuestas a la pregunta(2)

Su respuesta a la pregunta