Вопрос или проблема
Я пытаюсь создать шахматного бота. Я использую битовые доски для представления доски и фигур. Я читал в интернете из нескольких источников, что использование PEXT для поиска движений фигур делает ваш код действительно быстрым. Я реализовал свою версию этого, и она на самом деле на 25% медленнее, чем моя оригинальная реализация (которая рассчитывала каждую атаку с помощью логических операций).
Вот новый код, который использует PEXT для вычисления атаки:
uint64_t Move_lookup::get_rook_moves(int index, uint64_t all_pieces) {
int index2 = _pext_u64(all_pieces, rook_masks[index]);
return rook_moves[index][index2];
}
Моя таблица поиска – это двумерный вектор. Первый индекс – это начальный индекс фигуры, а второй индекс – это конкретный шаблон атаки в зависимости от расположения других фигур. Этот индекс получается с помощью PEXT. Вот как я определяю свою таблицу поиска:
std::vector<std::vector<uint64_t>> rook_moves;
А вот моя оригинальная реализация (в данный момент не используется):
uint64_t Board::get_rook_moves(uint64_t* b, pieceColour colour, int n) {
// Мы должны выполнять операцию на наборе, с которым была вызвана функция
uint64_t rooks = *b;
isolate_piece(&rooks, n); // Это убирает все 1, которые нас не интересуют в слонах
// Получаем все вражеские фигуры
uint64_t enemy_pieces = all_pieces(invert_colour(colour));
// Объединенные фигуры для обнаружения препятствий
uint64_t black_white_pieces = all_pieces(colour) | enemy_pieces;
uint64_t attacks = 0ULL;
// Нам нужно смотреть во всех четырех прямых направлениях (С, В, Ю, З) пока не натолкнемся на край доски или другую фигуру
// Север
uint64_t move = rooks;
while (move & NOT_RANK_8) { // Продолжаем, пока не на последней ранге
move <<= 8; // Двигаемся на одну рангу вверх
if (move & black_white_pieces) { // Останавливаемся, если встречаем фигуру
if (move & enemy_pieces) attacks |= move; // Если вражеская фигура, включаем в атаки
break;
}
attacks |= move; // Добавляем квадрат к атакам
}
// Восток
move = rooks;
while (move & NOT_H_FILE) { // Продолжаем, пока не на файле H
move >>= 1; // Двигаемся на одну файл вправо
if (move & black_white_pieces) { // Останавливаемся, если встречаем фигуру
if (move & enemy_pieces) attacks |= move; // Если вражеская фигура, включаем в атаки
break;
}
attacks |= move; // Добавляем квадрат к атакам
}
// Юг
move = rooks;
while (move & NOT_RANK_1) { // Продолжаем, пока не на первой ранге
move >>= 8; // Двигаемся на одну рангу вниз
if (move & black_white_pieces) { // Останавливаемся, если встречаем фигуру
if (move & enemy_pieces) attacks |= move; // Если вражеская фигура, включаем в атаки
break;
}
attacks |= move; // Добавляем квадрат к атакам
}
// Запад
move = rooks;
while (move & NOT_A_FILE) { // Продолжаем, пока не на файле A
move <<= 1; // Двигаемся на один файл влево
if (move & black_white_pieces) { // Останавливаемся, если встречаем фигуру
if (move & enemy_pieces) attacks |= move; // Если вражеская фигура, включаем в атаки
break;
}
attacks |= move; // Добавляем квадрат к атакам
}
return attacks;
}
Вот код, как я заполняю свою таблицу атаки слона (обратите внимание, что это выполняется только один раз, в начале):
void Move_lookup::fill_rook_moves() {
uint64_t i = 1ULL;
for (auto& mask : rook_masks) {
int count = count_set(mask);
std::vector<uint64_t> perms = bin_n_perms_asc(count); // Получаем все перестановки битов
std::vector<uint64_t> temp;
for (const auto& perm : perms) {
uint64_t blockers = _pdep_u64(perm, mask); // Располагаем перестановки на блокерах
uint64_t attacks = 0ULL;
// Нам нужно смотреть во всех четырех прямых направлениях (С, В, Ю, З) пока не натолкнемся на край доски или блокер
// Север
uint64_t move = i;
while (move & NOT_RANK_8) { // Продолжаем, пока не на последней ранге
move <<= 8; // Двигаемся на одну рангу вверх
attacks |= move; // Добавляем квадрат к атакам
if (move & blockers) break;
}
// Восток
move = i;
while (move & NOT_A_FILE) { // Продолжаем, пока не на файле A
move >>= 1; // Двигаемся на одну файл вправо
attacks |= move; // Добавляем квадрат к атакам
if (move & blockers) break;
}
// Юг
move = i;
while (move & NOT_RANK_1) { // Продолжаем, пока не на первой ранге
move >>= 8; // Двигаемся на одну рангу вниз
attacks |= move; // Добавляем квадрат к атакам
if (move & blockers) break;
}
// Запад
move = i;
while (move & NOT_H_FILE) { // Продолжаем, пока не на файле H
move <<= 1; // Двигаемся на один файл влево
attacks |= move; // Добавляем квадрат к атакам
if (move & blockers) break;
}
temp.push_back(attacks);
}
rook_moves.push_back(temp);
i <<= 1;
}
}
По-видимому, таблицы поиска работают медленно, потому что массив, в котором хранятся значения, огромен – в нем более 100000 элементов, но, тем не менее, каждая реализация, использующая PEXT для поиска, требует таблицы такого размера. (Правка – Каждая реализация, которая не использует оптимизации по размеру)
Кто-нибудь знает, как я могу ускорить свои таблицы поиска?
Большое спасибо,
Майкл
Ответ или решение
Для ускорения поиска в вашей программе шахматного бота, использующего битовые доски и операции PEXT, необходимо рассмотреть несколько аспектов оптимизации кода и структуры данных.
1. Оптимизация структуры данных
Ваши текущие данные о ходах слона хранятся в двумерном векторе std::vector<std::vector<uint64_t>>
. Хотя это обеспечивает гибкость, доступ к элементам может быть менее быстрым, чем прямое обращение к массиву. Рассмотрите возможность использования одномерного массива, где доступ к элементам по индексу будет производиться быстрее:
std::vector<uint64_t> rook_moves; // Вектор будет хранить заранее заполненные данные.
2. Сжатие таблицы ходов
Ваша таблица с более чем 100000 элементами может быть избыточной. Можно попробовать компактизировать таблицу с использованием не только PEXT, но и битовых операций для объединения некоторых значений. Например, используйте более эффективные структуры данных, такие как хэш-таблицы, которые могут помочь снизить количество операций по сравнению с доступом к большому массиву.
3. Алгоритмические улучшения
Ваш текущий код для генерирования хода пешки смотрится достаточно исчерпывающе и эффективен, однако при использовании PEXT стоит обратить внимание на его производительность, так как за одно обращение к функции вы могли бы задействовать более одного вызова к ожидаемым данным. При генерации битов-шахов лучше использовать оптимизированные стратегии обхода, такие как итерации по квадратам поля, нежели вычисление через mask-и.
4. Использование SIMD-оптимизаций
Если ваша платформа поддерживает инструкции SIMD (например, AVX или SSE), рассмотрите возможность реализации параллельной обработки для вычисления всех возможных ходов слона за один проход. Это может значительно увеличить пропускную способность вашей программы, минимизируя количество тактов, необходимых для выполнения операций.
5. Профилирование
Используйте инструменты профилирования, такие как Valgrind или gprof, для анализа производительности вашего кода. Это поможет понять, какие части кода являются узкими местами, и на какие фрагменты стоит обратить внимание в первую очередь.
6. Минимизация вызовов PEXT
В вашем коде вы часто вызываете функцию PEXT для получения индекса, который затем используется для доступа к элементам массива. Это может оказывать негативное влияние на производительность. Вместо этого можно использовать кэшированное хранение результатов вычисления и сокращение вызовов функции, если в этом есть смысл.
Заключение
Оптимизация вашего шахматного бота будет включать несколько шагов: переработка структуры хранения данных, минимизация числа вызовов функций, максимальное использование возможностей аппаратного обеспечения и потенциала параллельных вычислений, а также постоянное доведение до оптимума алгоритмов движения фигур. Все эти аспекты помогут вам достичь значительных улучшений в производительности вашего кода.