Являются ли операторы переполнения менее эффективными, чем выполнение операций, которые не приводят к переполнениям?

Что я делаю: я пишу шахматный движок на Swift. Одной из наиболее важных частей написания сильного шахматного движка является способность генерировать как можно больше возможных будущих позиций на доске за минимально возможное время. Чем больше позиций ваш движок может сгенерировать и оценить за более короткий промежуток времени, тем он мощнее.

При этом я написал функции для генерации ходов для скользящих фигур (слонов, ладей и ферзей). Эти функции используют операторы переполнения (&+, &-, &*), как и обычные побитовые операторы часто вызывают ошибки переполнения.

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

//occupied is a bitboard that represents every square on the chess board that is occupied
//this value is set somewhere before our move generation functions are ever called
var occupied: UInt64 = 0

//the rankMasks and fileMasks are simply arrays of bitboards that represent each individual file and rank on a chess board
//rankMasks8[0] would represent the squares a8-h8, rankMasks8[1] would represent the squares a7-h7
//fileMasks8[0] would represent the squares a1-a8, fileMasks8[1] would represent the squares b1-b8
let rankMasks8: [UInt64] = [ 255, 65280, 16711680, 4278190080, 1095216660480, 280375465082880, 71776119061217280, 18374686479671623680 ]
    let fileMasks8: [UInt64] = [ 72340172838076673, 144680345676153346, 289360691352306692, 578721382704613384, 1157442765409226768, 2314885530818453536, 4629771061636907072, 9259542123273814144 ]

...

//We pass a square (0 - 63) as s and we are returned a UInt64, the bitboard representing all the squares that the piece on the passed square can move to.
func horizontalAndVerticalMoves(s: Int) -> UInt64 {
    //convert the passed square into a bitboard that represents its location, by raising 2 to the power of s
    let binaryS: UInt64 = 1<<s

    //formula for generating possible horizontal moves
    let possibilitiesHorizontal: UInt64 = (occupied &- (2 &* binaryS)) ^ UInt64.reverse(UInt64.reverse(occupied) &- 2 &* UInt64.reverse(binaryS))
    //formula for generating vertical moves
    let possibilitiesVertical: UInt64 = ((occupied & fileMasks8[s % 8]) &- (2 &* binaryS)) ^ UInt64.reverse(UInt64.reverse(occupied & fileMasks8[s % 8]) &- (2 &* UInt64.reverse(binaryS)))

    //we return possible horizontal moves OR possible vertical moves
    return (possibilitiesHorizontal & rankMasks8[s / 8]) | (possibilitiesVertical & fileMasks8[s % 8])
}

Единственная важная вещь, которую вам нужно знать об этой функции, это то, что она дает нам ожидаемый результат и делает это с помощью операторов переполнения.

Теперь моя предыдущая итерация того же метода (до того, как я понял, как обойти переполнения с помощью операторов переполнения) была намного более растянутой. Требовалось запустить четыре цикла while, которые удалялись бы от текущей фигуры в направлении «север», «юг», «восток» или «запад», пока не соприкасались с фигурой, блокирующей дальнейшее движение в соответствующем направлении. . Вот как выглядела эта итерация функции horizontalAndVerticalMoves:

func horizontalAndVerticalMoves(s: Int) -> UInt64 {
    let rankMask: UInt64 = rankMasks8[s/8]
    let fileMask: UInt64 = fileMasks8[s%8]
    let pseudoPossibleMoves: UInt64 = rankMask ^ fileMask
    var unblockedRanks: UInt64 = 0
    var unblockedFiles: UInt64 = 0
    var direction: Direction! = Direction.north

    var testingSquare: Int = s - 8
    while direction == .north {
        if testingSquare < 0 || testingSquare%8 != s%8 {
            direction = .east
        } else {
            if 1<<testingSquare&occupied != 0 {
                unblockedRanks += rankMasks8[testingSquare/8]
                direction = .east
            } else {
                unblockedRanks += rankMasks8[testingSquare/8]
                testingSquare -= 8
            }
        }
    }

    testingSquare = s + 1
    while direction == .east {
        if testingSquare > 63 || testingSquare/8 != s/8 {
            direction = .south
        } else {
            if 1<<testingSquare&occupied != 0 {
                unblockedFiles += fileMasks8[testingSquare%8]
                direction = .south
            } else {
                unblockedFiles += fileMasks8[testingSquare%8]
                testingSquare += 1
            }
        }
    }

    testingSquare = s + 8
    while direction == .south {
        if testingSquare > 63 || testingSquare%8 != s%8 {
            direction = .west
        } else {
            if 1<<testingSquare&occupied != 0 {
                unblockedRanks += rankMasks8[testingSquare/8]
                direction = .west
            } else {
                unblockedRanks += rankMasks8[testingSquare/8]
                testingSquare += 8
            }
        }
    }

    testingSquare = s - 1
    while direction == .west {
        if testingSquare < 0 || testingSquare/8 != s/8 {
            direction = .north
        } else {
            if 1<<testingSquare&occupied != 0 {
                unblockedFiles += fileMasks8[testingSquare%8]
                direction = .north
            } else {
                unblockedFiles += fileMasks8[testingSquare%8]
                testingSquare -= 1
            }
        }
    }

    let mask = unblockedRanks | unblockedFiles
    let possibleMoves = pseudoPossibleMoves&mask

    return possibleMoves
}

Я полагал, что моя недавно реализованная версия этой функции (та, которая использует операторы переполнения) будет не только более краткой, но и гораздо более эффективной. Единственное важное, что вам нужно отметить в этой итерации той же функции, это то, что она дает нам ожидаемый результат, но выглядит гораздо более растянутой и не использует операторы переполнения.

Что я заметил. Как уже упоминалось, я ожидал, что мой новый, более чистый код, использующий операторы переполнения, будет работать намного быстрее, чем итерация, использующая множество циклов while. Выполняя тесты, чтобы увидеть, насколько быстро я могу генерировать шахматные ходы, я обнаружил, что версия, использующая циклы while вместо операторов переполнения, была значительно быстрее. Вычисление каждой комбинации из первых трех ходов в шахматной партии занимает у исходной функции чуть меньше 6 секунд, в то время как у более новой функции, использующей операторы переполнения занимают чуть меньше 13 секунд.

Что меня интересует. Поскольку я хочу создать максимально мощный шахматный движок, я ищу фрагменты моего кода, которые я могу ускорить. Старая функция, работающая быстрее, чем новая, кажется мне нелогичной. Итак, мне интересно, является ли оператор переполнения в Swift общеизвестно медленным/неэффективным?

Вот как выглядит рассматриваемый класс, который генерирует эти ходы: https://github.com/ChopinDavid/Maestro/blob/master/Maestro/Moves.swift


person David Chopin    schedule 11.03.2020    source источник
comment
На самом деле операторы, перехватывающие переполнение, будут медленнее, потому что им нужно ввести дополнительную ветвь, которая проверяет и перехватывает переполнение. Я бы посоветовал вам профилировать его и посмотреть. Абстрактные вопросы производительности в значительной степени неуместны   -  person Alexander    schedule 11.03.2020


Ответы (1)


Итак, мне интересно, является ли оператор переполнения в Swift общеизвестно медленным/неэффективным?

Нет, если что-то наоборот может быть правдой.

Инструкции машинного уровня для умножения и т. д. могут устанавливать флаг переполнения, но не более того. Для стандартных операторов Swift должен скомпилировать дополнительные инструкции для проверки этого флага и генерации ошибки, и этот код включает ветки (хотя предсказание ветвлений должно эффективно смягчить их).

Код для вашей версии оператора переполнения короче, чем для стандартной версии оператора, а также без веток.

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

Вероятно, вам нужно искать разницу в производительности в другом месте. Хорошей охоты!

Примечание: приведенное выше сравнение основано на полностью оптимизированном коде ("Самый быстрый, самый маленький [-Os]"), созданном компилятором Swift в Xcode 11.3.1, отладочная сборка может дать совсем другие результаты.

person CRD    schedule 11.03.2020
comment
Черт, нажми на пост, и затем ТАК покажет мне комментарий @Alexander-ReinstateMonica, написанный 30 минут назад... Ну ладно. - person CRD; 11.03.2020
comment
Интересно, мое расширение UInt64.reverse(), которое меняет порядок битов UInt64. По крайней мере, это единственная вещь в моем новом коде, которая, как я вижу, может вызывать проблему, так как остальная часть - это почти только операции. - person David Chopin; 11.03.2020
comment
На самом деле это точно????????‍♂️ UInt64.reverse() перебирает все 64 бита и переупорядочивает их. Пришло время найти более эффективный способ изменить порядок битов в Swift! - person David Chopin; 11.03.2020