mboost-dp1
Opfinderen af assembler død i en alder af 100
- Forside
- ⟨
- Forum
- ⟨
- Tagwall
#2
Jeg startede med Compass assembler på CDC Cyber under NOS - det var en vanskelig arkitektur: word adresser ikke byte adresser, ikke virtuel memory, 60 bit words og 6 bit bytes, mix af 60 bit registre og 18 bit registre, load/store model.
Næste platform var meget nemmere. Macro-32 på VAX under VMS. Det mest CISC'ede instruktion-sæt, byte adresser, virtuel memory, 8 bit bytes, 32 bit registre, direct memory access model. Jeg skrev meget kode der. Jeg vil tro at jeg i perioden 86-96 nåede over 50000 linier.
Jeg startede med Compass assembler på CDC Cyber under NOS - det var en vanskelig arkitektur: word adresser ikke byte adresser, ikke virtuel memory, 60 bit words og 6 bit bytes, mix af 60 bit registre og 18 bit registre, load/store model.
Næste platform var meget nemmere. Macro-32 på VAX under VMS. Det mest CISC'ede instruktion-sæt, byte adresser, virtuel memory, 8 bit bytes, 32 bit registre, direct memory access model. Jeg skrev meget kode der. Jeg vil tro at jeg i perioden 86-96 nåede over 50000 linier.
#4 Wow, det var nogle gamle systemer. Hvordan tastede man assemblerkoden ind den gang, var der en konsol eller en tele type? Eller snakker vi hulkort?
Jeg har prøvet assembler på en del forskellige arkitekturer, x86 (DOS PC, på hobby plan), Intel 8051 (universitetsprojekt), MOS 6502 (Commodore 64), PIC-16 (ewwww), Atmel AVR (universitet), og en Freescale/Motorola 68HCS12 derivativ (arbejdsrelateret).
8051 er noget værre skrammel, der er ikke en rigtig stak til funktionsparametre, så en C compiler må lave alle mulige tricks for at simulere det. Men af en eller anden grund var det en populær CPU at komme i microcontrollere der havde forskellige specialformål som f.eks. RF. Jeg tænker at det er en IP fri arkitektur, eller at den er meget billig at licensiere.
PIC-16 er også noget værre skrammel, og jeg har kun helt undtagelsesvist måtte reverse engineere noget maskinkode til den. Vi brugte en C compiler, hvor jeg løb ind i en decideret compiler bug. Det var nok noget af det mest frustrerende jeg har oplevet i min karriere, for normalt tager man for givet at kode eksekverer som skrevet. Ikke her. Fejlsøgningen var et mareridt.
Mit yndlingsinstruktionssæt var klart 68HCS12. Her kunne man skrive virkelig kompakt og næsten smuk assemblerkode. Jeg skulle tilføje ekstra funktioner til et gammelt projekt og der var ikke meget plads til overs, så med til opgaven var at gøre det så kompakt som muligt. Det var en sjov opgave, instruktionssættet er virkelig en fornøjelse.
Atmel AVR er også en lækker arkitektur, og med Arduino boards over det hele er det en ret udbredt platform. Hmm. Det kunne faktisk være en god begynder øvelse at skrive AVR assembler til en Arduino.
Jeg har prøvet assembler på en del forskellige arkitekturer, x86 (DOS PC, på hobby plan), Intel 8051 (universitetsprojekt), MOS 6502 (Commodore 64), PIC-16 (ewwww), Atmel AVR (universitet), og en Freescale/Motorola 68HCS12 derivativ (arbejdsrelateret).
8051 er noget værre skrammel, der er ikke en rigtig stak til funktionsparametre, så en C compiler må lave alle mulige tricks for at simulere det. Men af en eller anden grund var det en populær CPU at komme i microcontrollere der havde forskellige specialformål som f.eks. RF. Jeg tænker at det er en IP fri arkitektur, eller at den er meget billig at licensiere.
PIC-16 er også noget værre skrammel, og jeg har kun helt undtagelsesvist måtte reverse engineere noget maskinkode til den. Vi brugte en C compiler, hvor jeg løb ind i en decideret compiler bug. Det var nok noget af det mest frustrerende jeg har oplevet i min karriere, for normalt tager man for givet at kode eksekverer som skrevet. Ikke her. Fejlsøgningen var et mareridt.
Mit yndlingsinstruktionssæt var klart 68HCS12. Her kunne man skrive virkelig kompakt og næsten smuk assemblerkode. Jeg skulle tilføje ekstra funktioner til et gammelt projekt og der var ikke meget plads til overs, så med til opgaven var at gøre det så kompakt som muligt. Det var en sjov opgave, instruktionssættet er virkelig en fornøjelse.
Atmel AVR er også en lækker arkitektur, og med Arduino boards over det hele er det en ret udbredt platform. Hmm. Det kunne faktisk være en god begynder øvelse at skrive AVR assembler til en Arduino.
larsp (5) skrev:
#4 Wow, det var nogle gamle systemer. Hvordan tastede man assemblerkoden ind den gang, var der en konsol eller en tele type? Eller snakker vi hulkort?
CDC NOS var gammel. Dog ikke hulkort. Terminal 9600 baud og en editor. Line mode editor altså - ligesom EDLIN på DOS for dem som kan huske!!
(der var en fuldskærms editor på systemet, men der var kun 2 skærme på instituttet der kunne køre den)
VMS var VT320 terminaler og normale tekst editorer (tekst baserede 24 x 80 eller 24 x 132 ikke GUI) som jeg stadig bruger (med terminal emulator ikke fysisk VT terminal).
Og VT320 terminalerne kørte serielt 19200 baud til en terminal-server og derefter ethernet (LAT protocol) til systemet.
Macro-32 demo:
$ type m1.c
#include <stdio.h>
int add1(int a, int b);
int add2(int *a, int *b);
int add1x(int a, int b);
int add2x(int *a, int *b);
int main(int argc, char *argv[])
{
int a, b;
a = 123;
b = 456;
printf("%d\n", add1(a, b));
printf("%d\n", add2(&a, &b));
printf("%d\n", add1x(a, b));
printf("%d\n", add2x(&a, &b));
return 0;
}
$ type add.c
int add1(int a, int b)
{
return a + b;
}
int add2(int *a, int *b)
{
return *a + *b;
}
$ type addx.mar
.title addx
.psect $CODE quad,pic,con,lcl,shr,exe,nowrt
.entry add1x,^m<>
addl3 4(ap),8(ap),r0
ret
.entry add2x,^m<>
addl3 @4(ap),@8(ap),r0
ret
.end
$ cc m1
$ cc add
$ macro addx
$ link m1 + add + addx
$ run m1
579
579
579
579
$ type m1.c
#include <stdio.h>
int add1(int a, int b);
int add2(int *a, int *b);
int add1x(int a, int b);
int add2x(int *a, int *b);
int main(int argc, char *argv[])
{
int a, b;
a = 123;
b = 456;
printf("%d\n", add1(a, b));
printf("%d\n", add2(&a, &b));
printf("%d\n", add1x(a, b));
printf("%d\n", add2x(&a, &b));
return 0;
}
$ type add.c
int add1(int a, int b)
{
return a + b;
}
int add2(int *a, int *b)
{
return *a + *b;
}
$ type addx.mar
.title addx
.psect $CODE quad,pic,con,lcl,shr,exe,nowrt
.entry add1x,^m<>
addl3 4(ap),8(ap),r0
ret
.entry add2x,^m<>
addl3 @4(ap),@8(ap),r0
ret
.end
$ cc m1
$ cc add
$ macro addx
$ link m1 + add + addx
$ run m1
579
579
579
579
#8
Der er en lille krølle.
Ovenstående er VAX assembler, men jeg har ikke assemblet det, men compilet det.
Forklaring følger. Der blev skrevet en del assembler kode på VAX tilbage i 80'erne. Så da DEC skiftede fra VAX til Alpha ca. 1992 var der et problem med al den assembler kode hos kunderne. DEC's løsning var en Macro-32 compiler til Alpha som kunne compile VAX assembler.
Så Macro-32:
.psect $CODE quad,pic,con,lcl,shr,exe,nowrt
.entry add1x,^m<>
addl3 4(ap),8(ap),r0
ret
.entry add2x,^m<>
addl3 @4(ap),@8(ap),r0
ret
er valid assembler på VAX, men på Alpha hvor jeg faktisk testede er det compilet.
Og det er compilet til (listing):
.PSECT $CODE, QUAD, PIC, CON, REL, LCL, SHR, EXE, RD, NOWRT
0000 ADD1X::
23DEFFE0 0000 LDA SP, -32(SP)
B77E0000 0004 STQ R27, (SP)
B75E0010 0008 STQ R26, 16(SP)
B7BE0018 000C STQ FP, 24(SP)
47FE041D 0010 MOV SP, FP
0014 $L1:
42110000 0014 ADDL R16, R17, R0 ; 000004
0018 $L2: ; 000005
47FD041E 0018 MOV FP, SP
A79D0010 001C LDQ R28, 16(FP)
A7BD0018 0020 LDQ FP, 24(FP)
23DE0020 0024 LDA SP, 32(SP)
6BFC8001 0028 RET R28
2FFE0000 002C UNOP
0030 ADD2X::
23DEFFE0 0030 LDA SP, -32(SP)
B77E0000 0034 STQ R27, (SP)
B75E0010 0038 STQ R26, 16(SP)
B7BE0018 003C STQ FP, 24(SP)
47FE041D 0040 MOV SP, FP
0044 $L3:
A3900000 0044 LDL R28, (R16) ; 000007
A3510000 0048 LDL R26, (R17)
439A0000 004C ADDL R28, R26, R0
0050 $L4: ; 000008
47FD041E 0050 MOV FP, SP
A79D0010 0054 LDQ R28, 16(FP)
A7BD0018 0058 LDQ FP, 24(FP)
23DE0020 005C LDA SP, 32(SP)
6BFC8001 0060 RET R28
Alpha er RISC og en VAX instruktion kræver adskillige Alpha instruktioner.
Der er en lille krølle.
Ovenstående er VAX assembler, men jeg har ikke assemblet det, men compilet det.
Forklaring følger. Der blev skrevet en del assembler kode på VAX tilbage i 80'erne. Så da DEC skiftede fra VAX til Alpha ca. 1992 var der et problem med al den assembler kode hos kunderne. DEC's løsning var en Macro-32 compiler til Alpha som kunne compile VAX assembler.
Så Macro-32:
.psect $CODE quad,pic,con,lcl,shr,exe,nowrt
.entry add1x,^m<>
addl3 4(ap),8(ap),r0
ret
.entry add2x,^m<>
addl3 @4(ap),@8(ap),r0
ret
er valid assembler på VAX, men på Alpha hvor jeg faktisk testede er det compilet.
Og det er compilet til (listing):
.PSECT $CODE, QUAD, PIC, CON, REL, LCL, SHR, EXE, RD, NOWRT
0000 ADD1X::
23DEFFE0 0000 LDA SP, -32(SP)
B77E0000 0004 STQ R27, (SP)
B75E0010 0008 STQ R26, 16(SP)
B7BE0018 000C STQ FP, 24(SP)
47FE041D 0010 MOV SP, FP
0014 $L1:
42110000 0014 ADDL R16, R17, R0 ; 000004
0018 $L2: ; 000005
47FD041E 0018 MOV FP, SP
A79D0010 001C LDQ R28, 16(FP)
A7BD0018 0020 LDQ FP, 24(FP)
23DE0020 0024 LDA SP, 32(SP)
6BFC8001 0028 RET R28
2FFE0000 002C UNOP
0030 ADD2X::
23DEFFE0 0030 LDA SP, -32(SP)
B77E0000 0034 STQ R27, (SP)
B75E0010 0038 STQ R26, 16(SP)
B7BE0018 003C STQ FP, 24(SP)
47FE041D 0040 MOV SP, FP
0044 $L3:
A3900000 0044 LDL R28, (R16) ; 000007
A3510000 0048 LDL R26, (R17)
439A0000 004C ADDL R28, R26, R0
0050 $L4: ; 000008
47FD041E 0050 MOV FP, SP
A79D0010 0054 LDQ R28, 16(FP)
A7BD0018 0058 LDQ FP, 24(FP)
23DE0020 005C LDA SP, 32(SP)
6BFC8001 0060 RET R28
Alpha er RISC og en VAX instruktion kræver adskillige Alpha instruktioner.
Interessant hvor meget en enkelt VAX add instruktion kan ende med at fylde i Alpha instruktioner. Jeg bemærker også at Alpha instruktionerne fylder ret meget.
Man kan vel sige at et CISC instruktionssæt er en form for "kompression" af binær kode ved at samme funktionalitet kan presses ned på færre bytes kode. I sidste ende bliver CISC instruktionerne alligevel oversat til en form for RISC internt i CPUen før det bliver eksekveret, så CISC er en mellemstation. Og der kan bestemt være en fordel i at have mindre binær kode. Det loader hurtigere og cacher bedre.
Man kunne have taget den idé og gået hele vejen ved at lade en CPU eksekvere f.eks. LZW komprimerede RISC instruktioner :)
Man kan vel sige at et CISC instruktionssæt er en form for "kompression" af binær kode ved at samme funktionalitet kan presses ned på færre bytes kode. I sidste ende bliver CISC instruktionerne alligevel oversat til en form for RISC internt i CPUen før det bliver eksekveret, så CISC er en mellemstation. Og der kan bestemt være en fordel i at have mindre binær kode. Det loader hurtigere og cacher bedre.
Man kunne have taget den idé og gået hele vejen ved at lade en CPU eksekvere f.eks. LZW komprimerede RISC instruktioner :)
#10
Alle Alpha instruktioner er 32 bit lange.
(den midterste kolonne er offset i koden)
Det er et typisk kendetegn ved RISC at alle instruktioner lige lange. Nogen mener at det er den bedste måde at se forskel på RISC of CISC - fast længde versus variabel længde instruktioner.
Fast længde instruktioner har nogle fordele ved at CPU kan starte med at analysere de næste instruktioner inden den er færdig med at analysere en instruktion.
Alle Alpha instruktioner er 32 bit lange.
(den midterste kolonne er offset i koden)
Det er et typisk kendetegn ved RISC at alle instruktioner lige lange. Nogen mener at det er den bedste måde at se forskel på RISC of CISC - fast længde versus variabel længde instruktioner.
Fast længde instruktioner har nogle fordele ved at CPU kan starte med at analysere de næste instruktioner inden den er færdig med at analysere en instruktion.
#10
En anden ting er at VAX instruktionsæt er langsomt fordi der er alt for meget memory access i.f.m. et kald.
Et banalt kald i C som:
foobar(1, 2, 3, 4);
bliver til noget a la:
pushl #4 ; 4 byte til stack
pushl #3 ; 4 byte til stack
pushl #2 ; 4 byte til stack
pushl #1 ; 4 byte til stack
calls #1,foobar
...
...
...
.entry foobar,^m<r2,r3,r4,r5> ; 9 x 4 bytes til stack
...
ret ; 9 x 4 bytes fra stack
selve kaldet har betydet 56 bytes til stack og 36 byte fra stack - det er meget memory access for et banalt funktions kald.
RISC har typisk flere registre og parametere overføres via registre.
På Alpha gemmes de første 6 argumenter i R16..R21.
Så ikke bare kan Alpha sekvensen analyseres mere parallelt og dermed hurtigere men den er også hurtigere fordi der ikke er den dyre VAX call/ret semantik.
En anden ting er at VAX instruktionsæt er langsomt fordi der er alt for meget memory access i.f.m. et kald.
Et banalt kald i C som:
foobar(1, 2, 3, 4);
bliver til noget a la:
pushl #4 ; 4 byte til stack
pushl #3 ; 4 byte til stack
pushl #2 ; 4 byte til stack
pushl #1 ; 4 byte til stack
calls #1,foobar
...
...
...
.entry foobar,^m<r2,r3,r4,r5> ; 9 x 4 bytes til stack
...
ret ; 9 x 4 bytes fra stack
selve kaldet har betydet 56 bytes til stack og 36 byte fra stack - det er meget memory access for et banalt funktions kald.
RISC har typisk flere registre og parametere overføres via registre.
På Alpha gemmes de første 6 argumenter i R16..R21.
Så ikke bare kan Alpha sekvensen analyseres mere parallelt og dermed hurtigere men den er også hurtigere fordi der ikke er den dyre VAX call/ret semantik.
Og hvis nu vi skal nørde lidt.
Jeg fik min Itanium box startet op (det tog lidt tid iLO havde glemt al konfiguration og mig og iLO er ikke gode venner!).
Så jeg compilede lige det stykke VAX assembler.
Og det gav:
.psect $CODE, CON, LCL, SHR, EXE, NOWRT, NOVEC, NOSHORT
.proc ADD1X
.align 32
.global ADD1X
ADD1X: // 000003
{ .mii
002C00B16A40 0000 alloc r41 = rspfs, 2, 9, 0, 0
010800C00740 0001 mov r29 = sp // r29 = r12
010800C00A80 0002 mov r42 = sp // r42 = r12
}
0010 $L1:
{ .mmi
010002042200 0010 add r8 = in1, in0 ;; // r8 = r33, r32 // 000004
01C0000003C0 0011 cmp.eq pr15, pr0 = r0, r0 // 000005
0000B0800200 0012 sxt4 r8 = r8 // 000004
}
0020 $L2: // 000005
{ .mmi
010802A00300 0020 mov sp = r42 ;; // r12 = r42
000008000000 0021 nop.m 0 Padding to fill a bundle.
000154052000 0022 mov.i rspfs = r41 ;;
}
{ .mfb
000008000000 0030 nop.m 0 Padding to fill a bundle.
000008000000 0031 nop.f 0 Padding to fill a bundle.
000108001100 0032 br.ret.sptk.many rp ;; // br0
}
.endp ADD1X
.proc ADD2X
.align 32
.global ADD2X
ADD2X: // 000006
{ .mii
002C00B16A40 0040 alloc r41 = rspfs, 2, 9, 0, 0
010800C00740 0041 mov r29 = sp // r29 = r12
010800C00A80 0042 mov r42 = sp // r42 = r12
}
0050 $L3:
{ .mmi
0108021003C0 0050 mov r15 = in1 ;; // r15 = r33 // 000007
0108020004C0 0051 mov r19 = in0 // r19 = r32
00A04A100200 0052 tbit.z pr8, pr9 = in1, 0 // pr8, pr9 = r33, 0
}
{ .mii
01C0000003C0 0060 cmp.eq pr15, pr0 = r0, r0 // 000008
010802100440 0061 mov r17 = in1 ;; // r17 = r33 // 000007
00A03A000180 0062 tbit.z pr6, pr7 = in0, 0 // pr6, pr7 = r32, 0
}
{ .mii
00A000F02489 0070 (pr9) ld1 r18 = [r15], 1
0108020005C0 0071 mov r23 = in0 // r23 = r32
000008000000 0072 nop.i 0 ;; Padding to fill a bundle.
}
{ .mmi
00A001302607 0080 (pr7) ld1 r24 = [r19], 1 ;;
00A040F04488 0081 (pr8) ld2 r18 = [r15], 2
000008000000 0082 nop.i 0 ;; Padding to fill a bundle.
}
{ .mmi
00A041304606 0090 (pr6) ld2 r24 = [r19], 2 ;;
00A040F04400 0091 ld2 r16 = [r15], 2
000008000000 0092 nop.i 0 ;; Padding to fill a bundle.
}
{ .mii
00A041304580 00A0 ld2 r22 = [r19], 2
0097F9220488 00A1 (pr8) dep r18 = r16, r18, 16, 16 ;;
0097F982C606 00A2 (pr6) dep r24 = r22, r24, 16, 16 ;;
}
{ .mib
000008000000 00B0 nop.m 0 Padding to fill a bundle.
009BF9220489 00B1 (pr9) dep r18 = r16, r18, 8, 16
004000000000 00B2 nop.b 0 Padding to fill a bundle.
}
{ .mmi
008000F00409 00C0 (pr9) ld1 r16 = [r15] ;;
000008000000 00C1 nop.m 0 Padding to fill a bundle.
009BF982C607 00C2 (pr7) dep r24 = r22, r24, 8, 16
}
{ .mmi
008001300587 00D0 (pr7) ld1 r22 = [r19] ;;
000008000000 00D1 nop.m 0 Padding to fill a bundle.
0093B9220489 00D2 (pr9) dep r18 = r16, r18, 24, 8 ;;
}
{ .mii
000008000000 00E0 nop.m 0 Padding to fill a bundle.
0093B982C607 00E1 (pr7) dep r24 = r22, r24, 24, 8
0000B1200480 00E2 sxt4 r18 = r18 ;;
}
{ .mii
000008000000 00F0 nop.m 0 Padding to fill a bundle.
0000B1800600 00F1 sxt4 r24 = r24 ;;
010001824200 00F2 add r8 = r18, r24 ;;
}
{ .mib
000008000000 0100 nop.m 0 Padding to fill a bundle.
0000B0800200 0101 sxt4 r8 = r8
004000000000 0102 nop.b 0 Padding to fill a bundle.
}
0110 $L4: // 000008
{ .mmi
010802A00300 0110 mov sp = r42 ;; // r12 = r42
000008000000 0111 nop.m 0 Padding to fill a bundle.
000154052000 0112 mov.i rspfs = r41 ;;
}
{ .mfb
000008000000 0120 nop.m 0 Padding to fill a bundle.
000008000000 0121 nop.f 0 Padding to fill a bundle.
000108001100 0122 br.ret.sptk.many rp ;; // br0
}
.endp ADD2X
Rigtigt meget kode.
Og hvis nogen synes at den ser mystisk ud så er forklaringen her.
Itanium er en EPIC computer en variant af VLIW computer.
En CPU core fødes med 128 bit instruction bundles som består af 3 instruktioner som så udføres parallelt.
Derfor syntaxen:
{ .xyz
instruktion 1
instruktion 2
instruktion 3
}
De 3 instruktioner loades og udføres parallelt.
xyz fortæller hvad de er: m=memory, i=integer, b=branch, f=floating point
Og hvis det ikke er muligt at udføre parallelt så indsættes der NOP instruktioner (og dem er der faktisk en del af i ovenstående).
Jeg fik min Itanium box startet op (det tog lidt tid iLO havde glemt al konfiguration og mig og iLO er ikke gode venner!).
Så jeg compilede lige det stykke VAX assembler.
Og det gav:
.psect $CODE, CON, LCL, SHR, EXE, NOWRT, NOVEC, NOSHORT
.proc ADD1X
.align 32
.global ADD1X
ADD1X: // 000003
{ .mii
002C00B16A40 0000 alloc r41 = rspfs, 2, 9, 0, 0
010800C00740 0001 mov r29 = sp // r29 = r12
010800C00A80 0002 mov r42 = sp // r42 = r12
}
0010 $L1:
{ .mmi
010002042200 0010 add r8 = in1, in0 ;; // r8 = r33, r32 // 000004
01C0000003C0 0011 cmp.eq pr15, pr0 = r0, r0 // 000005
0000B0800200 0012 sxt4 r8 = r8 // 000004
}
0020 $L2: // 000005
{ .mmi
010802A00300 0020 mov sp = r42 ;; // r12 = r42
000008000000 0021 nop.m 0 Padding to fill a bundle.
000154052000 0022 mov.i rspfs = r41 ;;
}
{ .mfb
000008000000 0030 nop.m 0 Padding to fill a bundle.
000008000000 0031 nop.f 0 Padding to fill a bundle.
000108001100 0032 br.ret.sptk.many rp ;; // br0
}
.endp ADD1X
.proc ADD2X
.align 32
.global ADD2X
ADD2X: // 000006
{ .mii
002C00B16A40 0040 alloc r41 = rspfs, 2, 9, 0, 0
010800C00740 0041 mov r29 = sp // r29 = r12
010800C00A80 0042 mov r42 = sp // r42 = r12
}
0050 $L3:
{ .mmi
0108021003C0 0050 mov r15 = in1 ;; // r15 = r33 // 000007
0108020004C0 0051 mov r19 = in0 // r19 = r32
00A04A100200 0052 tbit.z pr8, pr9 = in1, 0 // pr8, pr9 = r33, 0
}
{ .mii
01C0000003C0 0060 cmp.eq pr15, pr0 = r0, r0 // 000008
010802100440 0061 mov r17 = in1 ;; // r17 = r33 // 000007
00A03A000180 0062 tbit.z pr6, pr7 = in0, 0 // pr6, pr7 = r32, 0
}
{ .mii
00A000F02489 0070 (pr9) ld1 r18 = [r15], 1
0108020005C0 0071 mov r23 = in0 // r23 = r32
000008000000 0072 nop.i 0 ;; Padding to fill a bundle.
}
{ .mmi
00A001302607 0080 (pr7) ld1 r24 = [r19], 1 ;;
00A040F04488 0081 (pr8) ld2 r18 = [r15], 2
000008000000 0082 nop.i 0 ;; Padding to fill a bundle.
}
{ .mmi
00A041304606 0090 (pr6) ld2 r24 = [r19], 2 ;;
00A040F04400 0091 ld2 r16 = [r15], 2
000008000000 0092 nop.i 0 ;; Padding to fill a bundle.
}
{ .mii
00A041304580 00A0 ld2 r22 = [r19], 2
0097F9220488 00A1 (pr8) dep r18 = r16, r18, 16, 16 ;;
0097F982C606 00A2 (pr6) dep r24 = r22, r24, 16, 16 ;;
}
{ .mib
000008000000 00B0 nop.m 0 Padding to fill a bundle.
009BF9220489 00B1 (pr9) dep r18 = r16, r18, 8, 16
004000000000 00B2 nop.b 0 Padding to fill a bundle.
}
{ .mmi
008000F00409 00C0 (pr9) ld1 r16 = [r15] ;;
000008000000 00C1 nop.m 0 Padding to fill a bundle.
009BF982C607 00C2 (pr7) dep r24 = r22, r24, 8, 16
}
{ .mmi
008001300587 00D0 (pr7) ld1 r22 = [r19] ;;
000008000000 00D1 nop.m 0 Padding to fill a bundle.
0093B9220489 00D2 (pr9) dep r18 = r16, r18, 24, 8 ;;
}
{ .mii
000008000000 00E0 nop.m 0 Padding to fill a bundle.
0093B982C607 00E1 (pr7) dep r24 = r22, r24, 24, 8
0000B1200480 00E2 sxt4 r18 = r18 ;;
}
{ .mii
000008000000 00F0 nop.m 0 Padding to fill a bundle.
0000B1800600 00F1 sxt4 r24 = r24 ;;
010001824200 00F2 add r8 = r18, r24 ;;
}
{ .mib
000008000000 0100 nop.m 0 Padding to fill a bundle.
0000B0800200 0101 sxt4 r8 = r8
004000000000 0102 nop.b 0 Padding to fill a bundle.
}
0110 $L4: // 000008
{ .mmi
010802A00300 0110 mov sp = r42 ;; // r12 = r42
000008000000 0111 nop.m 0 Padding to fill a bundle.
000154052000 0112 mov.i rspfs = r41 ;;
}
{ .mfb
000008000000 0120 nop.m 0 Padding to fill a bundle.
000008000000 0121 nop.f 0 Padding to fill a bundle.
000108001100 0122 br.ret.sptk.many rp ;; // br0
}
.endp ADD2X
Rigtigt meget kode.
Og hvis nogen synes at den ser mystisk ud så er forklaringen her.
Itanium er en EPIC computer en variant af VLIW computer.
En CPU core fødes med 128 bit instruction bundles som består af 3 instruktioner som så udføres parallelt.
Derfor syntaxen:
{ .xyz
instruktion 1
instruktion 2
instruktion 3
}
De 3 instruktioner loades og udføres parallelt.
xyz fortæller hvad de er: m=memory, i=integer, b=branch, f=floating point
Og hvis det ikke er muligt at udføre parallelt så indsættes der NOP instruktioner (og dem er der faktisk en del af i ovenstående).
Ja, itanium er en ret ekstrem konstruktion med alle de registre og med parallelitet i instruktionssættet. De gik all-in på en bestemt filosofi og det viste sig ikke at fungere så fantastisk igen. Når det kommer til stykket er det bare mere praktisk at schedulerings hardware i CPUen bestemmer hvad de forskellige regneenheder skal tage sig af i runtime, end at en compiler skal bestemme det hele på forhånd. Compileren kender ikke altid hele sandheden om hvad der sker i CPUen. Der kan være forskellige ventetider for at hente data, der kan være andre tråde der banker på. Med en overflod af transistorer i moderne processorer er det ikke et problem at lave avanceret scheduleringslogik on-chip.
ARM instruktionssættet virker for mig som at have ramt en rigtig god balance mellem at være grundlæggende RISC, men alligevel have snedige features der gør at man kan pakke meget funktionalitet ind i færre instruktioner. Det gør koden mere kompakt.
Og med Apple's M1/M2... monster CPUer må man bare konstatere at ARM platformen har bevist at den kan levere en ekstrem god performance.
ARM instruktionssættet virker for mig som at have ramt en rigtig god balance mellem at være grundlæggende RISC, men alligevel have snedige features der gør at man kan pakke meget funktionalitet ind i færre instruktioner. Det gør koden mere kompakt.
Og med Apple's M1/M2... monster CPUer må man bare konstatere at ARM platformen har bevist at den kan levere en ekstrem god performance.
#18
Det er en kendsgerning at ideen aldrig har været nogen success i den virkelig verden.
Multiflow Trace systemerne tilbage i 80'erne var meget VLIW - de blev solgt i 3 udgaver som udførte henholdsvis 7, 14 og 28 instruktioner i parallel. Men de solgte slet ikke.
Itanium som blev kaldt EPIC og ikke VLIW af HP og Intel udførte kun 3 instruktioner i parallel. Og der blev solgt en del systemer. Der var support for OpenVMS, HP-UX, Windows, Linux etc.. Men den døde.
Den vigtigste årsag til at den døde er nok volumen. CPU design og byggeri af en CPU produktions facilitet koster mange milliarder af dollars. Det gør at de CPU'er som sælges i store antal udkonkurrerer dem der sælges i lille antal.
Matematikken er simpel:
antagelse: fast omkostning 5 B$ og variabel omkostning 50 $ per CPU
sælger 1 millioner CPU => total omkostning 5050 $ per CPU
sælger 100 millioner CPU => total omkostning 100 $ per CPU
(tallene er opdigtede men pointen er stadig valid)
Så Itanium døde p.g.a. lav volumen og x86-64 (og senere ARM) fik success fordi det store antal PC CPU gjorde server CPU billige (og ARM kunne sælge enorme mængder til mobiltelefoner).
Der var så også problemer med at få compilerne til at udnytte paralleliteten. Der var mig bekendt ingen compilere som kom bare i nærheden af at opfylde visionen for Itanium. Men hvis markedfet havde været der, så tror jeg at man havde fået compilerne.
Det er en kendsgerning at ideen aldrig har været nogen success i den virkelig verden.
Multiflow Trace systemerne tilbage i 80'erne var meget VLIW - de blev solgt i 3 udgaver som udførte henholdsvis 7, 14 og 28 instruktioner i parallel. Men de solgte slet ikke.
Itanium som blev kaldt EPIC og ikke VLIW af HP og Intel udførte kun 3 instruktioner i parallel. Og der blev solgt en del systemer. Der var support for OpenVMS, HP-UX, Windows, Linux etc.. Men den døde.
Den vigtigste årsag til at den døde er nok volumen. CPU design og byggeri af en CPU produktions facilitet koster mange milliarder af dollars. Det gør at de CPU'er som sælges i store antal udkonkurrerer dem der sælges i lille antal.
Matematikken er simpel:
antagelse: fast omkostning 5 B$ og variabel omkostning 50 $ per CPU
sælger 1 millioner CPU => total omkostning 5050 $ per CPU
sælger 100 millioner CPU => total omkostning 100 $ per CPU
(tallene er opdigtede men pointen er stadig valid)
Så Itanium døde p.g.a. lav volumen og x86-64 (og senere ARM) fik success fordi det store antal PC CPU gjorde server CPU billige (og ARM kunne sælge enorme mængder til mobiltelefoner).
Der var så også problemer med at få compilerne til at udnytte paralleliteten. Der var mig bekendt ingen compilere som kom bare i nærheden af at opfylde visionen for Itanium. Men hvis markedfet havde været der, så tror jeg at man havde fået compilerne.
#18
Der er to problemer med at lade CPU'en schedulere instruktionerne.
1) den er nødt til at have læst og fortolket alle instruktionerne inden den kan schedulere og det tager tid.
2) det giver problemer for compiler/runtime og concurrency når compileren ikke ved i hvilken rækkefølge instruktionerne bliver udført
Der er to problemer med at lade CPU'en schedulere instruktionerne.
1) den er nødt til at have læst og fortolket alle instruktionerne inden den kan schedulere og det tager tid.
2) det giver problemer for compiler/runtime og concurrency når compileren ikke ved i hvilken rækkefølge instruktionerne bliver udført
#22
Re 2)
Det er memory model og happens before jeg tænker på.
x86-64 er meget nem på dette område.
Itanium er halv slem og Alpha er stor slem på dette område. De laver faktisk noget instruction reordering og resultatet er stor risiko for concurrency problemer.
https://devblogs.microsoft.com/oldnewthing/2017081...
https://preshing.com/20120930/weak-vs-strong-memor...
Re 2)
Det er memory model og happens before jeg tænker på.
x86-64 er meget nem på dette område.
Itanium er halv slem og Alpha er stor slem på dette område. De laver faktisk noget instruction reordering og resultatet er stor risiko for concurrency problemer.
https://devblogs.microsoft.com/oldnewthing/2017081...
https://preshing.com/20120930/weak-vs-strong-memor...
Her er en `objdump -d add.o` efter `gcc -c -O1 add.c` for forskellige gcc arkitekturer. -O0 lavede ekstrem tumpet kode, -O1 synes en god balance for dette eksperiment.
add.c source:
Først avr-gcc. (Jeg har ændret int i add.c til long for at få 32-bit ordlængde):
Den kæmper en brav kamp i add2, den kære 8-bitter. Det er dyrt at kaste om sig med 32-bit på en 8-bit processor.
Her med arm-poky-linux-gnueabi-gcc fra en ARM Cortex-A9 toolchain:
Lad os slutte af med standard x64-64 gcc:
Det var da alligevel kompakt. Jeg er overrasket. Variable instruction length har nogle fordele.
add.c source:
int add1(int a, int b)
{
return a + b;
}
int add2(int *a, int *b)
{
return *a + *b;
}
Først avr-gcc. (Jeg har ændret int i add.c til long for at få 32-bit ordlængde):
avradd.o: file format elf32-avr
Disassembly of section .text:
00000000 <add1>:
0: 62 0f add r22, r18
2: 73 1f adc r23, r19
4: 84 1f adc r24, r20
6: 95 1f adc r25, r21
8: 08 95 ret
0000000a <add2>:
a: 0f 93 push r16
c: 1f 93 push r17
e: e6 2f mov r30, r22
10: f7 2f mov r31, r23
12: a8 2f mov r26, r24
14: b9 2f mov r27, r25
16: 4d 91 ld r20, X+
18: 5d 91 ld r21, X+
1a: 6d 91 ld r22, X+
1c: 7c 91 ld r23, X
1e: 80 81 ld r24, Z
20: 91 81 ldd r25, Z+1 ; 0x01
22: a2 81 ldd r26, Z+2 ; 0x02
24: b3 81 ldd r27, Z+3 ; 0x03
26: 04 2f mov r16, r20
28: 15 2f mov r17, r21
2a: 26 2f mov r18, r22
2c: 37 2f mov r19, r23
2e: 08 0f add r16, r24
30: 19 1f adc r17, r25
32: 2a 1f adc r18, r26
34: 3b 1f adc r19, r27
36: 93 2f mov r25, r19
38: 82 2f mov r24, r18
3a: 71 2f mov r23, r17
3c: 60 2f mov r22, r16
3e: 1f 91 pop r17
40: 0f 91 pop r16
42: 08 95 ret
Den kæmper en brav kamp i add2, den kære 8-bitter. Det er dyrt at kaste om sig med 32-bit på en 8-bit processor.
Her med arm-poky-linux-gnueabi-gcc fra en ARM Cortex-A9 toolchain:
add.o: file format elf32-littlearm
Disassembly of section .text:
00000000 <add1>:
0: e0800001 add r0, r0, r1
4: e12fff1e bx lr
00000008 <add2>:
8: e5900000 ldr r0, [r0]
c: e5913000 ldr r3, [r1]
10: e0800003 add r0, r0, r3
14: e12fff1e bx lr
Lad os slutte af med standard x64-64 gcc:
add.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <add1>:
0: 8d 04 37 lea (%rdi,%rsi,1),%eax
3: c3 retq
0000000000000004 <add2>:
4: 8b 06 mov (%rsi),%eax
6: 03 07 add (%rdi),%eax
8: c3 retq
Det var da alligevel kompakt. Jeg er overrasket. Variable instruction length har nogle fordele.
Ikke bare volumen. Mig bekendt havde itanium heller ikke nogen særlig performance fordel, hvis overhovedet. Den var dyr, klodset, og uden salgsargument.arne_v (21) skrev:Så Itanium døde p.g.a. lav volumen og x86-64 (og senere ARM) fik success fordi det store antal PC CPU gjorde server CPU billige (og ARM kunne sælge enorme mængder til mobiltelefoner).
Men hvorfor er det så svært at lave en compiler der kan udnytte parallelitet, når scheduleringshardware i dagens CPUer gør et glimrende job?arne_v (21) skrev:Der var så også problemer med at få compilerne til at udnytte paralleliteten. Der var mig bekendt ingen compilere som kom bare i nærheden af at opfylde visionen for Itanium. Men hvis markedfet havde været der, så tror jeg at man havde fået compilerne.
PS. Angående out-of-order eksekvering og concurrency problemer. Det er da en god pointe. Men jeg tænker at de har opfundet diverse SYNC instruktioner man kan sætte ind hvis der er et vigtigt skæringspunkt mht. concurrency?
larsp (25) skrev:Ikke bare volumen. Mig bekendt havde itanium heller ikke nogen særlig performance fordel, hvis overhovedet. Den var dyr, klodset, og uden salgsargument.arne_v (21) skrev:Så Itanium døde p.g.a. lav volumen og x86-64 (og senere ARM) fik success fordi det store antal PC CPU gjorde server CPU billige (og ARM kunne sælge enorme mængder til mobiltelefoner).
Den havde ikke nogen faktisk performance fordel, men tilbage i de tidlige 00'ere så det nu lidt lovende ud.
En 1 GHz Itanium baseret på 180 nm teknologi med ikke specielt gode compilere var ca. 10% langsommere for integer operationer og ca. 50% hurtigere for floating point operationer sammenlignet med en 2.5 Ghz x86-64 baseret på 130/90 nm teknologi.
Hvis der havde været investeret nok penge i Itanium CPU produktion til at de havde været på samme 130/90 nm teknologi med en højere clock frekvens og hvis der havde været investeret nok tid i compilere, så virker det plausibelt at Itanium havde været de 2-3 gange hurtigere som den var forventet.
Men hvis og hvis ændrer jo ikke på hvad der faktisk skete.
larsp (25) skrev:arne_v (21) skrev:
Der var så også problemer med at få compilerne til at udnytte paralleliteten. Der var mig bekendt ingen compilere som kom bare i nærheden af at opfylde visionen for Itanium. Men hvis markedfet havde været der, så tror jeg at man havde fået compilerne.
Men hvorfor er det så svært at lave en compiler der kan udnytte parallelitet, når scheduleringshardware i dagens CPUer gør et glimrende job?
Mange gange vanskelige opgave.
x86-64 ---- Itanium
stream of single instructions ---- stream of bundles of 3 instructions
low possibilities for instructions reordering per ISA definition ---- medium possibilities for reordering of instructions including across bundles per ISA definition
1 of each execution unit ---- 2 or 3 of some executions units
16 integer + 16 FP registers ---- 128 integer + 128 FP registers with visible register renaming
Men hvis pengene havde været der så var problemet blevet løst.
larsp (25) skrev:
PS. Angående out-of-order eksekvering og concurrency problemer. Det er da en god pointe. Men jeg tænker at de har opfundet diverse SYNC instruktioner man kan sætte ind hvis der er et vigtigt skæringspunkt mht. concurrency?
Selvfølgelig har de det. Ellers var det være umuligt at skrive pålidelig kode.
Alpha (der er den værste m.h.t. reorder) har en MB (Memory Barrier) instruktion til formålet.
Og i C kan man eksplicit indsætte en med et builtin funktions kald.
__MB();
Men det sker jo ikke via magi.
Enten skal compileren bestemme sig for at indsætte dem de rigtige steder f.eks. før og efter brug af variable erklæret volatile eller så skal programmøren selv indseætte dem.
Det første er ikke nemt. Og det sidste har jo en åbenlys risiko for at blive glemt.
#28
Et godt eksempel på hvor komplekst det kan være findes i .NET's historie.
I .NET 1.x brugte .NET bare x86 & x86-64 memory model (som er en stærk memory model med få muligheder for reorder).
Men i .NET 2.0 skulle de understøtte Itanium (de har droppet supporten siden) og fordi Itanioum har en mellem memory model (med mellem muligheder for reorder) så måtte de lave en .NET memory model som de kunne sikre gjaldt på både x86 & x86-64 og Itanium.
Et godt eksempel på hvor komplekst det kan være findes i .NET's historie.
I .NET 1.x brugte .NET bare x86 & x86-64 memory model (som er en stærk memory model med få muligheder for reorder).
Men i .NET 2.0 skulle de understøtte Itanium (de har droppet supporten siden) og fordi Itanioum har en mellem memory model (med mellem muligheder for reorder) så måtte de lave en .NET memory model som de kunne sikre gjaldt på både x86 & x86-64 og Itanium.
Opret dig som bruger i dag
Det er gratis, og du binder dig ikke til noget.
Når du er oprettet som bruger, får du adgang til en lang række af sidens andre muligheder, såsom at udforme siden efter eget ønske og deltage i diskussionerne.