Фен-бойчетата на C# и Java знаят, че техните компилатори превеждат до byte code -- някакво вътрешно представяне със специализирани инструкции, които виртуалната машина по някое време (най-често по време на изпълнението) превежда до машинен код за съответния процесор. В Microsoft средите на това му викат Common Language Runtime (CLR), а в Java средите не знам какво му викат, ама е точно толкова дебилно. Защо eval да не създава код, който директно да се изпълява на съответната физическа машина? Знам какво ще чуя: защото не е преносим. И какво от това -- нали сорс кода, бидейки прост текст, е преносим? И ако за съответната платформа има виртуална машина, която по зададен сорс код може да създаде вътрешно представяне, защо трябва да се интересувам от това, дали вътрешното представяне на една архитектура е същото като на друга? Просто разпространявам сорс кода и всичко е ОК. Естествено, това е в разрез с Enterprise мисленето, но пък моята теза е, че именно Enterprise мисленето е в основата на всичко зло. Така че това не е довод, поне не пред мен.
Аз използвам имплементацията SBCL -- Steel Bank Common Lisp. Последните две думи (Common Lisp) са ясни -- името на диалекта, за който си говорим. Първите две (Steel Bank) са бъзик с CMU-CL, чийто fork e SBCL. Университета Carnegie Mellon University (CMU) e именован на Andrew Carnegie -- крупен стоманен (Steel) магнат в Щатите в началото на XX век, и Andrew Mellon -- водещ банкер (Bank) по същото време.
В тази имплементация нещата седят по следния начин: всяко дефинране на функция води до създаването на списък от инструкции, които могат директно да се изпълнят на съответния процесор, на който се е случило това дефиниране. Това има следния недостатък -- ако компилирам нещо върху X86_64, няма да мога да го ftp-осам на PPC машина и да го изпълня там. Но на кой му пука за този use-case така или иначе? Затова пък кода, който е генериран не отстътва по бързодействие на аналогичен код, компилиран със C компилатор. И за да не съм голословен, ще приведа следния пример:
CL-USER> (defun bahor (i j) (+ i (* 2 j)))
BAHOR
Това вече трябва да е ясно на всички, чели предишния пост. Дефинираме функция bahor, която по зададени парметри i и j връща стойност i+2*j. Наблюдавайте сега:
CL-USER> (disassemble #'bahor)
; disassembly for BAHOR
; 05C102CD: 488B55F0 MOV RDX, [RBP-16]
; 2D1: BF04000000 MOV EDI, 4
; 2D6: 4C8D1C25B9020020 LEA R11, [#x200002B9]
; 2DE: 41FFD3 CALL R11
; 2E1: 480F42E3 CMOVB RSP, RBX
; 2E5: 488BFA MOV RDI, RDX
; 2E8: 488B55F8 MOV RDX, [RBP-8]
; 2EC: 4C8D1C25E0010020 LEA R11, [#x200001E0]
; 2F4: 41FFD3 CALL R11
; 2F7: 480F42E3 CMOVB RSP, RBX
; 2FB: 488BE5 MOV RSP, RBP
; 2FE: F8 CLC
; 2FF: 5D POP RBP
; 300: C3 RET
; 301: CC0A BREAK 10
; 303: 02 BYTE #X02
; 304: 18 BYTE #X18
; 305: 54 BYTE #X54
NIL
Опаааа -- какво имаме тук? Първо -- имаме стандартна функция disasseble, която изпечатва листинг на машинния код + асемблера за съответната функция (само помислете колко софтуер трябва да инсталирате, за да получите същата информация за дадена функция на C, не дай си боже на Java). И второ -- машинния код както можем да видим е съставен от инструкции за (в случая) 64-битов X86 процесор. И понеже имам достъп и до PowerPC машина, ето как изглежда аналогичния изход там:
CL-USER> (disassemble #'bahor)
; disassembly for BAHOR
; 5119667C: 92EF0004 STW $LRA,4($CFP)
; 680: 830F0010 LWZ $A0,16($CFP)
; 684: 3B200008 ADDI $A1,$ZERO,8
; 688: 3AF30070 ADDI $LRA,$CODE,112
; 68C: 3CE00400 ADDIS $NL4,$ZERO,1024
; 690: 38E70360 ADDI $NL4,$NL4,864
; 694: 7CE803A6 MTLR $NL4
; 698: 4E800020 BLR
; 69C: 00000000 BYTE #X00, #X00, #X00, #X00
; 6A0: 00001C36 BYTE #X00, #X00, #X1C, #X36
; 6A4: 7ED0B378 MR $CSP,$OCFP
; 6A8: 60000000 NOP
; 6AC: 3A77FF90 ADDI $CODE,$LRA,-112
; 6B0: 7F19C378 MR $A1,$A0
; 6B4: 830F000C LWZ $A0,12($CFP)
; 6B8: 3AF300A0 ADDI $LRA,$CODE,160
; 6BC: 3CA00400 ADDIS $NL2,$ZERO,1024
; 6C0: 38A50200 ADDI $NL2,$NL2,512
; 6C4: 7CA803A6 MTLR $NL2
; 6C8: 4E800020 BLR
; 6CC: 00000000 BYTE #X00, #X00, #X00, #X00
; 6D0: 00002836 BYTE #X00, #X00, #X28, #X36
; 6D4: 7ED0B378 MR $CSP,$OCFP
; 6D8: 60000000 NOP
; 6DC: 3A77FF60 ADDI $CODE,$LRA,-160
; 6E0: 806F0000 LWZ $NL0,0($CFP)
; 6E4: 82EF0004 LWZ $LRA,4($CFP)
; 6E8: 7DF07B78 MR $CSP,$CFP
; 6EC: 7C6F1B78 MR $CFP,$NL0
; 6F0: 3BF70005 ADDI $LIP,$LRA,5
; 6F4: 7FE803A6 MTLR $LIP
; 6F8: 4E800020 BLR
; 6FC: 00000000 BYTE #X00, #X00, #X00, #X00
NIL
Вижда се, че SBCL компилира/интерпретира до машинен код. Т.е. на въпроса колко виртуална е виртуалната му машина -- отговора е "много малко виртуална е".
В този момент C фанатиците доволно потриват ръце и си викат "толкова много код за едно умножение и едно събиране? -- това очевидно не е оптимално". В който момент аз почвам да обяснявам как този код е в състояние да обработи всякакви стойности за i, j и върнатия резултат, стига да се събират в паметта, независимо дали са цели числа, рационални дроби или floating point и давам следния пример:
CL-USER> (bahor 10000000000000000000000000000000000000000000000000000000000 7/19)
190000000000000000000000000000000000000000000000000000000014/19
(това между другото е числото 10 на 58-ма степен + два пъти по 7/19, което е равно на рационалното число, получено като резултат на последния ред)
Но те продължават -- "Е да де, ама аз не искам такива сложнотии, аз искам i и j да са простички цели числа, които се събират в регистри на съответния процесор; резултатa и той се събира в такъв регистър, сигурен съм! И тогава това е overkill, защото не ме интересува колко е всеобхватно, аз искам да е бързо!"
За такива случаи в Common Lisp е предвидено програмистът да може да укаже на компилатора какви всъщност са типовете на променливите. Но забележете -- само с цел оптимизация, а не промяна на семантиката! Функцията ще работи и без да й указвате какви са типовете на променливите; ако ги укажете може просто да заработи по-бързо, но крайния резултат ще е един и същ!
И така, нека да опитаме да укажем типовете, като променим кода да изглежда така:
(defun bahor (i j)
(declare (optimize (speed 3) (safety 0)) (fixnum i j))
(the fixnum (+ i (* 2 j))))
Имаме един допълнителен ред, който започва с declare, в който се казват следните неща преведени на Български: компилаторе, искам да оптимизираш тази функция за бързодействие (speed 3), без да се интересуваш дали типовете на аргументите, които ти подавам, съответстват на истината (safety 0) и променливите i и j са от тип fixnum (Без да изпадам в подробности ще кажа, че съгласно стандарта, променливи от тип fixnum се събират в регистър). Другата промяна е на последния ред, в който казваме "... върнатия резултат и той ще се събере в регистър". И тогава (на x86-64):
CL-USER> (disassemble #'bahor)
; disassembly for BAHOR
; 0351C4F5: 48D1E7 SHL RDI, 1
; 4F8: 4801FA ADD RDX, RDI
; 4FB: 48D1E2 SHL RDX, 1
; 4FE: 488BE5 MOV RSP, RBP
; 501: F8 CLC
; 502: 5D POP RBP
; 503: C3 RET
NIL
CL-USER>
CL-USER> (disassemble #'bahor)
; disassembly for BAHOR
; 0351C4F5: 48D1E7 SHL RDI, 1
; 4F8: 4801FA ADD RDX, RDI
; 4FB: 48D1E2 SHL RDX, 1
; 4FE: 488BE5 MOV RSP, RBP
; 501: F8 CLC
; 502: 5D POP RBP
; 503: C3 RET
NIL
CL-USER>
а на PowerPC:
CL-USER> (disassemble #'bahor)
; disassembly for BAHOR
; 5119D20C: 54A6083C RLWINM $NL3,$NL2,1,0,30
; 10: 7CC43214 ADD $NL3,$NL1,$NL3
; 14: 54D8103A RLWINM $A0,$NL3,2,0,29
; 18: 7DF07B78 MR $CSP,$CFP
; 1C: 7ECFB378 MR $CFP,$OCFP
; 20: 3BF70005 ADDI $LIP,$LRA,5
; 24: 7FE803A6 MTLR $LIP
; 28: 4E800020 BLR
; 2C: 00000000 BYTE #X00, #X00, #X00, #X00
NIL
CL-USER> (disassemble #'bahor)
; disassembly for BAHOR
; 5119D20C: 54A6083C RLWINM $NL3,$NL2,1,0,30
; 10: 7CC43214 ADD $NL3,$NL1,$NL3
; 14: 54D8103A RLWINM $A0,$NL3,2,0,29
; 18: 7DF07B78 MR $CSP,$CFP
; 1C: 7ECFB378 MR $CFP,$OCFP
; 20: 3BF70005 ADDI $LIP,$LRA,5
; 24: 7FE803A6 MTLR $LIP
; 28: 4E800020 BLR
; 2C: 00000000 BYTE #X00, #X00, #X00, #X00
NIL
Смея да твърдя, че това е съизмеримо с кода, който генерира произволен C компилатор за подобна функция. И какво излиза -- ако искаме всеобхватност, просто пишем кода; той работи за всякакви случаи; ако искаме бързодействие и знаем какво правим, можем да го декларираме и ще получим каквото искаме. Ето това за мен е мощна и смислена платформа -- по принцип може да работи за всички случаи, а ако съм много умен, може да го накарам да работи и бързо. А не като Java-та -- каквото и да правиш, все е бавно...
Лека нощ, драги зрители!
3 коментара:
това с dynamic types май още не мога да се съглася че е достатъчно безопасно, но да видим.
Затова е (safety 0). Ако му пуснеш нещо различно от fixnum ще получиш нещо различно от верния резултат :-)
А пък ако махнеш (safety 0), тогава ще сложи код, който проверява дали верно му пускаш каквото трябва и ще хвърля ексепшъни ако не е...
Публикуване на коментар