Почему случайный дополнительный код повышает производительность?

Struct Node {
    Node *N[SIZE];
    int value;
};

struct Trie {
    Node *root;

    Node* findNode(Key *key) {
        Node *C = &root;
        char u;
        while (1) {
            u = key->next();
            if (u < 0) return C;
         // if (C->N[0] == C->N[0]); // this line will speed up execution significantly
            C = C->N[u];
            if (C == 0) return 0;
        }
    }
    void addNode(Key *key, int value){...};
};

В этой реализации дерева префиксов (он же Trie) я обнаружил, что 90% времени выполнения findNode() занимает одна операция C=C->N[u];

Пытаясь ускорить этот код, я случайным образом добавил строку, закомментированную во фрагменте выше, и код стал на 30% быстрее! Почему это?

ОБНОВИТЬ

Вот полная программа.

#include "stdio.h"
#include "sys/time.h"

long time1000() {
  timeval val;
  gettimeofday(&val, 0);
  val.tv_sec &= 0xffff;
  return val.tv_sec * 1000 + val.tv_usec / 1000;
}

struct BitScanner {
    void *p; 
    int count, pos;
    BitScanner (void *p, int count) {
        this->p = p;
        this->count = count;
        pos = 0;
    }
    int next() {
        int bpos = pos >> 1;
        if (bpos >= count) return -1;
        unsigned char b = ((unsigned char*)p)[bpos];
        if (pos++ & 1) return (b >>= 4);
        return b & 0xf;
    }

};

struct Node {
    Node *N[16];
    __int64_t value;
    Node() : N(), value(-1) { }
};

struct Trie16 {
    Node root;

    bool add(void *key, int count, __int64_t value) {
        Node *C = &root;
        BitScanner B(key, count);
        while (true) {
            int u = B.next();
            if (u < 0) {
                if (C->value == -1) {
                    C->value = value;
                    return true; // value added
                }
                C->value = value;
                return false; // value replaced
            }
            Node *Q = C->N[u];
            if (Q) {
                C = Q;
            } else {
                C = C->N[u] = new Node;
            }
        }
    }

    Node* findNode(void *key, int count) {
        Node *C = &root;
        BitScanner B(key, count);
        while (true) {
            char u = B.next();
            if (u < 0) return C;
//          if (C->N[0] == C->N[1]);
            C = C->N[0+u];
            if (C == 0) return 0;
        }
    }
};

int main() {
    int T = time1000();
    Trie16 trie;
    __int64_t STEPS = 100000, STEP = 500000000, key;
    key = 0;
    for (int i = 0; i < STEPS; i++) {
        key += STEP;
        bool ok = trie.add(&key, 8, key+222);
    }
    printf("insert time:%i\n",time1000() - T); T = time1000();
    int err = 0;
    key = 0;
    for (int i = 0; i < STEPS; i++) {
        key += STEP;
        Node *N = trie.findNode(&key, 8);
        if (N==0 || N->value != key+222) err++;
    }
    printf("find time:%i\n",time1000() - T); T = time1000();
    printf("errors:%i\n", err);
}

person exebook    schedule 16.07.2015    source источник
comment
Какие флаги компиляции вы использовали? И вы сделали несколько тестов или только один?   -  person Aleksandar    schedule 16.07.2015
comment
Скорость доступа к памяти является распространенным узким местом в наши дни, когда все остальное быстро. Остерегайтесь этих ->, они могут быть очень дорогими.   -  person Kerrek SB    schedule 16.07.2015
comment
@Aleksandar, я провел несколько тестов, на самом деле сотни, это меня позабавило и привлекло мое внимание на несколько часов. Я использовал как clang, так и gcc с параметрами -O0 и -O3.   -  person exebook    schedule 16.07.2015
comment
Вы пробовали смотреть сборку?   -  person LPs    schedule 16.07.2015
comment
@exebook Откуда у вас появилась идея добавить код в попытке увеличить скорость?   -  person JorenHeit    schedule 16.07.2015
comment
Вы должны опубликовать MCVE.   -  person juanchopanza    schedule 16.07.2015
comment
@JorenHeit, я занимался отладкой и хотел подсчитать количество обращений к указателю, и обнаружил, что счетчик, добавленный для отладки, значительно повышает производительность.   -  person exebook    schedule 16.07.2015
comment
@exebook Это действительно странно!   -  person JorenHeit    schedule 16.07.2015
comment
Я думаю, что улучшение связано не с dummy++, а с if (C->N[0] == C->N[1]). Я предполагаю, что эта проверка приводит к тому, что кеши используются, и данные для C = C->N[u] сразу считываются в кеше.   -  person LPs    schedule 16.07.2015
comment
С небольшими программами это обычное поведение из-за разного содержимого кэша инструкций. Мое предложение состоит в том, чтобы скомпилировать и сохранить оба двоичных файла и запустить их один за другим, а затем изменить порядок и сравнить время.   -  person Cobusve    schedule 16.07.2015
comment
Каково значение/тип SIZE?   -  person edmz    schedule 16.07.2015
comment
Может быть, ЦП начинает выполнять предварительную выборку N, когда видит, что к нему обращаются несколько раз?   -  person Alexander Balabin    schedule 16.07.2015
comment
Я создал полный пример, я должен добавить его в сообщение?   -  person exebook    schedule 16.07.2015
comment
@exebook, да, это даст возможность воспроизвести его   -  person Petr    schedule 16.07.2015
comment
@Cobusve, попробовал, эффект сохраняется.   -  person exebook    schedule 16.07.2015
comment
Я только что узнал, что dummy++ часть избыточна, одной if (C->N[0] == C->N[1]); достаточно, чтобы вызвать эффект.   -  person exebook    schedule 16.07.2015
comment
Не могли бы вы попробовать заменить случайный код на __builtin_prefetch на gcc? gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html   -  person erenon    schedule 16.07.2015
comment
@erenon, я заменил случайный код на __builtin_prefetch(C->N[u]);, и эффект тот же. На самом деле даже на 5% быстрее.   -  person exebook    schedule 16.07.2015
comment
@exebook: Значит, проблема решена?   -  person erenon    schedule 16.07.2015
comment
@erenon, верно, но я не уверен, что должен принять ответ Александра Балабина.   -  person exebook    schedule 16.07.2015
comment
Ключевая строка бездействия отличается между исходным сообщением и обновлением. В исходном посте вы написали if (C->N[0] == C->N[0]);, который проверяет тавтологию, а затем ничего не делает. Я ожидаю, что компилятор оптимизирует это (при условии отсутствия побочных эффектов от перегрузок операторов). С другой стороны, if (C->N[0] == C->N[1]); не является тавтологией и может повлиять на кэширование, прогнозирование ветвлений, спекулятивное выполнение и т. д.   -  person Adrian McCarthy    schedule 16.07.2015
comment
@AdrianMcCarthy, но оба имеют одинаковый эффект.   -  person exebook    schedule 16.07.2015


Ответы (3)


Это в значительной степени предположение, но из того, что я читал о предварительной выборке данных ЦП, она будет выполнять предварительную выборку только в том случае, если увидит множественный доступ к одной и той же ячейке памяти, и этот доступ соответствует триггерам предварительной выборки, например, выглядит как сканирование. В вашем случае, если есть только один доступ к C->N, предварительная выборка не будет интересна, однако, если их несколько, и он может предсказать, что более поздний доступ будет дальше в тот же бит памяти, что может заставить его предварительно выбрать более одной строки кеша .

Если бы это происходило, то C->N[u] не нужно было бы ждать поступления памяти из ОЗУ, поэтому это было бы быстрее.

person Alexander Balabin    schedule 16.07.2015
comment
Правильный! Как я уже говорил, волшебство творит if (C->N[0] == C->N[1]), а не dummy++; - person LPs; 16.07.2015

Похоже, что вы делаете, чтобы предотвратить остановку процессора, откладывая выполнение кода до тех пор, пока данные не будут доступны локально.

Выполнение этого таким образом очень подвержено ошибкам, вряд ли будет продолжать работать последовательно. Лучший способ - заставить компилятор сделать это. По умолчанию большинство компиляторов генерируют код для универсального семейства процессоров. НО если вы посмотрите на доступные флаги, вы обычно можете найти флаги для указания вашего конкретного процессора, чтобы он мог генерировать более конкретный код (например, предварительную выборку и код остановки).

См.: GCC: чем марш отличается от mtune? второй ответ содержит некоторые подробности: https://stackoverflow.com/a/23267520/14065

person Martin York    schedule 16.07.2015
comment
FTR: в gcc и clang ищите -march=native. - person erenon; 16.07.2015

Поскольку каждая операция записи обходится дороже, чем чтение. Здесь Если вы это видите, C = C->N[u]; это означает, что ЦП выполняет запись на каждой итерации для переменной C. Но когда вы выполняете if (C->N[0] == C->N[1]) dummy++; запись на фиктивную выполняется, только если C->N[0] == C->N[1]. Таким образом, вы сохранили много инструкций записи процессора, используя условие if.

person userNishant    schedule 16.07.2015
comment
Если говорить об инструкциях процессора, dummy++ и C = C-›N[u]; будет иметь тот же смысл. - person userNishant; 16.07.2015