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

Я пишу программу для анализа графика социальной сети. Это означает, что программе требуется много случайных обращений к памяти. Мне кажется, что предварительная выборка должна помочь. Вот небольшая часть кода считывания значений соседей вершины.

for (size_t i = 0; i < v.get_num_edges(); i++) {
    unsigned int id = v.neighbors[i];
    res += neigh_vals[id];
}

Я преобразую код выше в тот, который указан ниже, и предварительно выберем значения соседей вершины.

int *neigh_vals = new int[num_vertices];

for (size_t i = 0; i < v.get_num_edges(); i += 128) {
    size_t this_end = std::min(v.get_num_edges(), i + 128);
    for (size_t j = i; j < this_end; j++) {
        unsigned int id = v.neighbors[j];
        __builtin_prefetch(&neigh_vals[id], 0, 2);
    }
    for (size_t j = i; j < this_end; j++) {
        unsigned int id = v.neighbors[j];
        res += neigh_vals[id];
    }
}

В этом коде на С++ я не переопределял никаких операторов.

К сожалению, код действительно не улучшает производительность. Интересно, почему. По-видимому, аппаратная предварительная выборка не работает в этом случае, потому что аппаратное обеспечение не может предсказать местоположение памяти.

Интересно, вызвано ли это оптимизацией GCC. Когда я компилирую код, я включаю -O3. Я действительно надеюсь, что предварительная выборка может еще больше повысить производительность даже при включенном -O3. В этом случае оптимизация -O3 сплавляет две петли? Может ли -O3 включить предварительную выборку в этом случае по умолчанию?

Я использую gcc версии 4.6.3, и программа работает на Intel Xeon E5-4620.

Спасибо, Da

Ответ 1

Да, некоторые недавние версии GCC (например, 4.9 в марте 2015 года) могут выдать некоторую инструкцию PREFETCH при оптимизации с -O3 (даже без явного __builtin_prefetch)

Мы не знаем, что делает get_neighbor, и каковы типы v и neigh_val.

И предварительная выборка не всегда выгодна. Добавление явного __builtin_prefetch может замедлить ваш код. Вам нужно измерить.

Как Пенсионный ниндзя прокомментировал, предварительная выборка в одном цикле и наделение данных будут кэшироваться в следующем цикле (далее в исходном коде) неверно.

Возможно, вы можете попробовать вместо этого

for (size_t i = 0; i < v.get_num_edges(); i++) {
  fg::vertex_id_t id = v.get_neighbor(i);
  __builtin_prefetch (neigh_val[v.get_neighbor(i+4)]);
  res += neigh_vals[id];
}

Вы можете эмпирически заменить 4 любой подходящей константой.

Но я полагаю, что __builtin_prefetch выше бесполезно (поскольку компилятор, вероятно, может добавить его сам по себе), и он может нанести вред (или даже сбой программы, когда вычисление его аргумента дает поведение undefined, например, если v.get_neighbor(i+4) - undefined, однако предварительная выборка адреса за пределами вашего адресного пространства не повредит, но может замедлить работу вашей программы). Проверочный тест.

См. этот ответ по соответствующему вопросу.

Обратите внимание, что в С++ все [], get_neighbor могут быть перегружены и становятся очень сложными операциями, поэтому мы не можем догадаться!

И есть случаи, когда аппаратное обеспечение ограничивает производительность, независимо от того, что вы добавляете __builtin_prefetch (и добавление их может повредить производительность)

BTW, вы можете передать -O3 -mtune=native -fdump-tree-ssa -S -fverbose-asm, чтобы понять, что делает компилятор (и посмотреть созданные сгенерированные файлы дампа и файлы ассемблера); также случается, что -O3 производит несколько более медленный код, чем дает -O2.

Вы можете рассмотреть явное многопоточность, OpenMP, OpenCL, если у вас есть время тратить время на оптимизацию. Помните, что преждевременная оптимизация - это зло. Вы оценили, прокомментировали ли вы все свое приложение?