首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >控制台TicTacToe实现

控制台TicTacToe实现
EN

Code Review用户
提问于 2018-06-10 08:57:26
回答 1查看 109关注 0票数 5

我对程序集很陌生,我想知道如何通过速度和/或二进制大小优化以及使代码易于读取和支持来改进这个实现。

平台:Linux,x64

汇编程序:NASM

代码节.data

代码语言:javascript
复制
; Consts
%define XChar 'X'
%define OChar 'O'
%define filler '+'
%define LF 0x0a
%define separatorChars " -:,.;"

%define stdin 0x00
%define stdout 0x01

X: db XChar
O: db OChar
currentTurn: db XChar

; Buffers.
field: times 9 db filler

; Strings.
lineFeed: db LF
coordSeparators: db separatorChars
coordSeparatorsL: equ $ - coordSeparators

choosePlayerStr: db "Choose your character - type X or O: "
choosePlayerStrL: equ $ - choosePlayerStr
makeTurn: db 'Enter cell coord delimited by space: ',
makeTurnL: equ $ - makeTurn
boundsError: db 'Coords should be in range 1-3.', LF
boundsErrorL: equ $ - boundsError
cellUsedError: db 'Cell on specified coords is already used.', LF
cellUsedErrorL: equ $ - cellUsedError
separatorNotExistsError: db "Use one of '", separatorChars, "' characters between the coords.", LF
separatorNotExistsErrorL: equ $ - separatorNotExistsError
winStr: db " win!", LF
winStrL: equ $ - winStr
separatorStr: db "---------------------------", LF
separatorStrL: equ $ - separatorStr
drawStr: db "Draw!", LF
drawStrL: equ $ - drawStr

section .bss
    buf: resb 256 ; Buffer for user input.

section .text

%macro writeChar 1
    mov rax, 0x01
    mov rdi, stdout
    mov rsi, %1
    mov rdx, 0x01
    syscall
%endmacro

%macro writeConsole 1
    mov rax, 0x01
    mov rdi, stdout
    mov rsi, %1
    mov rdx, %1L
    syscall
%endmacro

%macro readConsole 0
    mov rax, 0x00
    mov rdi, stdin
    mov rsi, buf
    mov rdx, 256
    syscall
%endmacro

global _start
_start:
    call printField

chooseCharacter:
    writeConsole choosePlayerStr
    readConsole

    cmp byte [buf], XChar
    je characterChosen

    cmp byte [buf], OChar
    je characterChosen

    jmp chooseCharacter

characterChosen:
    mov al, [buf]
    mov byte [currentTurn], al

nextTurn:
    writeConsole makeTurn
    readConsole

    mov rcx, coordSeparatorsL
checkCoordSeparator:
    mov al, byte [buf + 1]
    cmp al, byte [rcx + coordSeparators - 1]
    je processUserInput

    dec rcx
    cmp rcx, 0
    jg checkCoordSeparator

    writeConsole separatorNotExistsError
    jmp nextTurn

processUserInput:
    movzx r8, byte [buf]
    movzx r9, byte [buf + 2]
    sub r8, '0'
    sub r9, '0'

    call makeMove

    ; Save move result (success/fail) in r12, because other registers will
    ; be used by printField of affected by syscall.
    mov r12, rax
    call printField

    ; If move was failed - make turn again.
    cmp r12, 0
    jne nextTurn

checkForDraw:
    mov rcx, 9 ; Max number of loops
    mov al, filler
    mov rdi, field
    repne scasb

    cmp rcx, 0
    je draw

    ; Check for win.
    ; 1. Check columns.
    xor cl, cl; Counter.

nextColumn:
    mov al, byte [rcx + field] ; Copy 1st cell to al.

    ; Cell shouldn't contain filler.
    cmp al, filler
    je endIteration

    cmp al, byte [rcx + field + 3]
    jne endIteration

    cmp al, byte [rcx + field + 6]
    je win

endIteration:
    inc rcx
    cmp rcx, 0x03
    jl nextColumn

    ; 2. Check rows.
    xor cl, cl ; Counter.

nextRow:
    mov al, byte [rcx * 3 + field] ; Copy 1st cell to al.

    ; Cell shouldn't contain filler.
    cmp al, filler
    je endRowIteration

    cmp al, byte [rcx * 3 + field + 1]
    jne endRowIteration

    cmp al, byte [rcx * 3 + field + 2]
    je win

endRowIteration:
    inc rcx
    cmp rcx, 0x03
    jl nextRow

    ; 3. Check diagonals.
    ; 3.1. Check 1st (\) diagonal.
    mov al, byte [field] ; Copy 1st cell to al.

    ; Cell shouldn't contain filler.
    cmp al, filler
    je secondDiagonalCheck

    cmp al, byte [field + 4]
    jne secondDiagonalCheck

    cmp al, byte [field + 8]
    je win

; 3.2. Check 2nd (/) diagonal.
secondDiagonalCheck:
    mov al, byte [field + 2] ; Copy 1st cell to al.

    ; Cell shouldn't contain filler.
    cmp al, filler
    je endDiagonalCheck

    cmp al, byte [field + 4]
    jne endDiagonalCheck

    cmp al, byte [field + 6]
    je win

endDiagonalCheck:
    ; Change current player.
    cmp byte [currentTurn], XChar
    cmove rax, [O]
    cmovne rax, [X]
    mov byte [currentTurn], al

    jmp nextTurn

draw:
    writeConsole separatorStr
    writeConsole drawStr
    jmp exit

win:
    writeConsole separatorStr
    writeChar currentTurn
    writeConsole winStr

exit:
    ; return 0
    mov rax, 60
    mov rdi, 0x00
    syscall

; ================================================
; void printField()
printField:
    xor bx, bx

printRow:
    mov rax, 0x01
    mov rdi, 0x01
    lea rsi, [ebx + ebx * 2 + field]
    mov rdx, 0x03
    syscall

    writeChar lineFeed

    inc bx
    cmp bx, byte 3
    jnz printRow

    ret
; ================================================

; ================================================
; makeMove(int8 row, int8 col)
;
; r8: row coord
; r9: column coord
makeMove:
    ; Indexing start with 0, then we should decrement both
    dec r8
    dec r9

    ; Checking bounds.
    cmp r8, 0
    jl outOfBoundsError
    cmp r9, 0
    jl outOfBoundsError
    cmp r8, 3
    jge outOfBoundsError
    cmp r9, 3
    jge outOfBoundsError

    ; Check if used.
    mov rsi, r8
    add rsi, r9
    cmp byte [rsi + r8*2 + field], filler
    jne usedCellError

    ; Set cell.
    mov rax, [currentTurn]
    mov byte [rsi + r8*2 + field], al

    xor rax, rax
    jmp makeMoveEnd

; Errors.
outOfBoundsError:
    writeConsole boundsError

    mov rax, 1
    jmp makeMoveEnd

usedCellError:
    writeConsole cellUsedError

    mov rax, 1
    jmp makeMoveEnd

makeMoveEnd:
    ret
; ================================================

Codeflow (不太详细)

EN

回答 1

Code Review用户

发布于 2018-06-14 23:01:29

1)当寄存器为零时,执行xor eax, eax所需的代码比mov eax, 0少,而且运行速度(微观上)更快。还有一个窍门:虽然eax通常只是被认为是“较低的32位rax”,但是如果您执行xor eax, eax,它会隐式地对所有64位位进行归零,并将汇编成更少的字节。

2)查看此代码:

代码语言:javascript
复制
cmp byte [buf], OChar
je characterChosen

jmp chooseCharacter

characterChosen:

考虑像这样重组它:

代码语言:javascript
复制
cmp byte [buf], OChar
jne chooseCharacter

characterChosen:

如果它不使用jne,它只会继续到下一条语句。因此,这样做可以避免执行额外的跳转。

3)考虑这一守则:

代码语言:javascript
复制
dec rcx
cmp rcx, 0
jg checkCoordSeparator

cmp的工作方式是设置一些标志。在执行jg时,它会查看标志的当前值,以确定要做什么。但是cmp并不是唯一设置标志的指令。dec也是。在这种情况下,您可以尝试如下:

代码语言:javascript
复制
dec rcx
jnz checkCoordSeparator

每当您发现自己正在执行cmp时,请检查您是否刚刚做了一些数学运算,以便为您设置标志,这样您就可以节省时间,不再设置它们。

例如,有时您可以重构您的循环:而不是从0上升到3(与3相比),您可以从3下降到0,并使用这个技巧。它所做的工作量是相同的,但执行的指令却少了一个。

4)我不确定在'rcx‘中有什么,但它可能不需要"rcx“可以容纳的全部64位。如果您不期望这个值大于大约2,147,483,647,那么使用32位指令"ecx“可以节省一些费用。请注意,向下移动到16位版本(cx)并没有得到更多的节省,而且可能会更糟。

5)“当时我不确定'rcx‘中有什么内容”--我不确定的原因是你没有任何评论。Asm对人类来说尤其难读。而不是使用人类可以理解的名称(即"NumberOfMoves"),而是使用注册名称(即'rcx')。更糟糕的是,由于只有少数寄存器,所以在没有阅读所有代码的情况下,在任何给定的点上使用它们都不是很清楚。

这就是为什么写得很好的汇编程序充满了注释;每行的结尾通常都是一条注释,上面写着它在做什么(比如)。

虽然您可能清楚这段代码是做什么的(因为您刚刚编写了它),但是当其他人阅读您的代码时(或者您从现在起6个月后再读一次),这会使事情变得容易得多。

6)然后是这样的:

代码语言:javascript
复制
cmp rcx, 0
je draw

在检查寄存器是否为零时,此代码(稍微)更小:

代码语言:javascript
复制
test rcx, rcx
je draw

它可以汇编成一个更少的字节(如果可以使用ecx而不是rcx,则减少2个字节)。

请注意,这些建议首先(稍微)改进了代码的大小,并(不知不觉地)提高了代码的性能,但它们不会改变结果。这并不是说你正在做的事情会给出错误的答案,而是因为这不是最有效的方法。奇怪的是,你似乎在某些地方使用这些方法,而不是在其他地方?

IAC,现在,用高级语言(如C)编写代码,让编译器为您解决所有这些问题,几乎总是更好。他们已经知道了所有这些技巧(还有一千多个)。学习汇编语言是有用的,但是你学得越多,你就越意识到人类不应该再用汇编语言写东西了。

票数 2
EN
页面原文内容由Code Review提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://codereview.stackexchange.com/questions/196220

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档