• [^] # Re: Perlissade

    Posté par (site web personnel) . En réponse au journal C'est décidé, j'apprends Common Lisp!. Évalué à 4.

    Pas vraiment la façon dont je la pense mais comme programmeur OCaml expérimenté je peux dire que le cycle de développement avec Common Lisp est complètement différent de ce qu'il est avec OCaml.

    La raison principale est la proximité de l'évaluation du programme avec l'écriture de celui-ci. En OCaml on peut utiliser la REPL pour mettre au point une fonction mais c'est assez compliqué à mettre en place parcequ'il faut écrire un .ocamlinit spécifique au projet qui charge les modules nécessaires au module où vit notre fonction et installe les pretty-printers. Si on se rend compte qu'on a besoin du débogueur, il faut refaire toute cette configuration avec le débogueur. Enfin, une fois qu'on est content de sa fonction on peut continuer la mise au point avec une autre fonction d'un autre module et cela demande d'ajuster les fichiers de configuration pour le toplevel ou le débogueur à la main. En Common Lisp tout ce petit yoga disparaît, ce qui est du à l'association du débogueur à la REPL et à la portée dynamique des variables. Ce type de développement très interactif est semble-t-il un des points forts de Common Lisp et la norme parmi les programmeurs, c'est en tout cas une façon très agréable de travailler.

    Même si les PPE de OCaml sont très puissants et relativement faciles à utiliser – moyennant compilation et installation, puis paramétrage de la REPL pour les utiliser, et je ne sais pas si le débogueur les prend en charge – il faut bien reconnaître que la fonction équivalente en Common Lisp fournie par les macros est, en comparaison, d'utilisation enfantine.

    Un point sur lequel OCaml et Common Lisp se rapprochent est qu'en dépit de leur expressivité ces langages restent assez proches de la machine, ils laissent entrevoir très nettement les rouages de la machine.

    Comme en OCaml, le code assembleur généré (ici par SBCL) est très lisible:

    (defun my-average (numbers)
     (do ((tail numbers (rest tail))
     (n 0 (+ 1 n))
     (s 0 (+ s (first tail))))
     ((null tail) (if (eql n 0) 0 (/ s n)))))
    (disassemble 'my-average)
    ; disassembly for MY-AVERAGE
    ; Size: 182 bytes. Origin: #x100718AAA8
    ; AA8: 498B4C2460 MOV RCX, [R12+96] ; thread.binding-stack-pointer
     ; no-arg-parsing entry point
    ; AAD: 48894DF8 MOV [RBP-8], RCX
    ; AB1: 488B5DF0 MOV RBX, [RBP-16]
    ; AB5: 31C9 XOR ECX, ECX
    ; AB7: 31F6 XOR ESI, ESI
    ; AB9: EB61 JMP L1
    ; ABB: 0F1F440000 NOP
    ; AC0: L0: 488BD3 MOV RDX, RBX
    ; AC3: 8D43F9 LEA EAX, [RBX-7]
    ; AC6: A80F TEST AL, 15
    ; AC8: 0F8588000000 JNE L4
    ; ACE: 488B4201 MOV RAX, [RDX+1]
    ; AD2: 488945E8 MOV [RBP-24], RAX
    ; AD6: 488975E0 MOV [RBP-32], RSI
    ; ADA: 48895DD8 MOV [RBP-40], RBX
    ; ADE: BF02000000 MOV EDI, 2
    ; AE3: 488BD1 MOV RDX, RCX
    ; AE6: 41BBB0020020 MOV R11D, #x200002B0 ; GENERIC-+
    ; AEC: 41FFD3 CALL R11
    ; AEF: 4C8BC2 MOV R8, RDX
    ; AF2: 4C8945D0 MOV [RBP-48], R8
    ; AF6: 488B5DD8 MOV RBX, [RBP-40]
    ; AFA: 488B75E0 MOV RSI, [RBP-32]
    ; AFE: 488B7BF9 MOV RDI, [RBX-7]
    ; B02: 488BD6 MOV RDX, RSI
    ; B05: 41BBB0020020 MOV R11D, #x200002B0 ; GENERIC-+
    ; B0B: 41FFD3 CALL R11
    ; B0E: 4C8B45D0 MOV R8, [RBP-48]
    ; B12: 488B5DE8 MOV RBX, [RBP-24]
    ; B16: 498BC8 MOV RCX, R8
    ; B19: 488BF2 MOV RSI, RDX
    ; B1C: L1: 4881FB17001020 CMP RBX, 537919511
    ; B23: 759B JNE L0
    ; B25: 4885C9 TEST RCX, RCX
    ; B28: 7508 JNE L3
    ; B2A: 31D2 XOR EDX, EDX
    ; B2C: L2: 488BE5 MOV RSP, RBP
    ; B2F: F8 CLC
    ; B30: 5D POP RBP
    ; B31: C3 RET
    ; B32: L3: 488BD6 MOV RDX, RSI
    ; B35: 488BF9 MOV RDI, RCX
    ; B38: 4883EC18 SUB RSP, 24
    ; B3C: 48896C2408 MOV [RSP+8], RBP
    ; B41: 488D6C2408 LEA RBP, [RSP+8]
    ; B46: B904000000 MOV ECX, 4
    ; B4B: 41BBE03CB021 MOV R11D, #x21B03CE0 ; #<FUNCTION SB-KERNEL:TWO-ARG-/>
    ; B51: 41FFD3 CALL R11
    ; B54: EBD6 JMP L2
    ; B56: L4: 0F0B0A BREAK 10 ; error trap
    ; B59: 2F BYTE #X2F ; OBJECT-NOT-LIST-ERROR
    ; B5A: 10 BYTE #X10 ; RDX
    ; B5B: 0F0B10 BREAK 16 ; Invalid argument count trap
    

    Un examen succinct montre que L1 est notre if final et le reste de la structure s'en déduit facilement. Pour une version un peu plus "bas niveau" peut être écrite en ajoutant des annotations:

    CL-USER> (defun my-average (numbers)
     (let ((s 0)
     (n (length numbers)))
     (declare (optimize (speed 3) (safety 0)))
     (declare (type fixnum n s))
     (dotimes (i n)
     (incf s (the fixnum (svref numbers i))))
     (if (eql n 0) (the fixnum 0) (the fixnum (nth-value 0 (floor s n))))))
    WARNING: redefining COMMON-LISP-USER::MY-AVERAGE in DEFUN
    MY-AVERAGE
    CL-USER> (disassemble 'my-average)
    ; disassembly for MY-AVERAGE
    ; Size: 149 bytes. Origin: #x10056CE504
    ; 04: 498B442460 MOV RAX, [R12+96] ; thread.binding-stack-pointer
     ; no-arg-parsing entry point
    ; 09: 488945F8 MOV [RBP-8], RAX
    ; 0D: 4C8945F0 MOV [RBP-16], R8
    ; 11: 498BD0 MOV RDX, R8
    ; 14: 4883EC18 SUB RSP, 24
    ; 18: 48896C2408 MOV [RSP+8], RBP
    ; 1D: 488D6C2408 LEA RBP, [RSP+8]
    ; 22: B902000000 MOV ECX, 2
    ; 27: 41BB6000B021 MOV R11D, #x21B00060 ; #<FUNCTION LENGTH>
    ; 2D: 41FFD3 CALL R11
    ; 30: 488BDA MOV RBX, RDX
    ; 33: 4C8B45F0 MOV R8, [RBP-16]
    ; 37: 31FF XOR EDI, EDI
    ; 39: 31C0 XOR EAX, EAX
    ; 3B: EB12 JMP L1
    ; 3D: 0F1F00 NOP
    ; 40: L0: 498B4C8001 MOV RCX, [R8+RAX*4+1]
    ; 45: 4801F9 ADD RCX, RDI
    ; 48: 488BF9 MOV RDI, RCX
    ; 4B: 4883C002 ADD RAX, 2
    ; 4F: L1: 4839D8 CMP RAX, RBX
    ; 52: 7CEC JL L0
    ; 54: 4885DB TEST RBX, RBX
    ; 57: 750B JNE L3
    ; 59: 31FF XOR EDI, EDI
    ; 5B: L2: 488BD7 MOV RDX, RDI
    ; 5E: 488BE5 MOV RSP, RBP
    ; 61: F8 CLC
    ; 62: 5D POP RBP
    ; 63: C3 RET
    ; 64: L3: 488BF7 MOV RSI, RDI
    ; 67: 4885DB TEST RBX, RBX
    ; 6A: 7427 JEQ L6
    ; 6C: 488BC7 MOV RAX, RDI
    ; 6F: 4899 CQO
    ; 71: 48F7FB IDIV RAX, RBX
    ; 74: 488D3C00 LEA RDI, [RAX+RAX]
    ; 78: 4885D2 TEST RDX, RDX
    ; 7B: 7502 JNE L5
    ; 7D: L4: EBDC JMP L2
    ; 7F: L5: 4885F6 TEST RSI, RSI
    ; 82: 7DF9 JNL L4
    ; 84: 48D1FF SAR RDI, 1
    ; 87: 4883EF01 SUB RDI, 1
    ; 8B: 48D1E7 SHL RDI, 1
    ; 8E: EBED JMP L4
    ; 90: 0F0B10 BREAK 16 ; Invalid argument count trap
    ; 93: L6: 0F0B0A BREAK 10 ; error trap
    ; 96: 09 BYTE #X09 ; DIVISION-BY-ZERO-ERROR
    ; 97: 39 BYTE #X39 ; RDI
    ; 98: 19 BYTE #X19 ; RBX
    

    L'assembleur est un peu moins facile à suivre (pour moi) mais on voit quand-même que le seul appel à une fonction (remote call) est celui fait à length pendant l'initialisation de la boucle et que dans la boucle, la seule instruction qui accède à la mémoire est celle en position F00 tout le reste ne consiste qu'en des opérations très rapides sur les registres et des sauts courts. À la fin on nettoie la pile – seule la partie après le IDIV reste pour moi très mystérieuse, mais la boucle critique est simple et claire, même avec mes connaissances rudimentaires en assembleur.