fix missing byte to long assignment, add c-bench-64 comparison

This commit is contained in:
Irmen de Jong
2025-10-10 00:01:54 +02:00
parent 51ae32d23e
commit bb8b44d9d0
15 changed files with 572 additions and 19 deletions

View File

@@ -193,3 +193,12 @@ For instance here's a well known space ship animated in 3D with hidden line remo
in the CommanderX16 emulator:
![cobra3d](docs/source/_static/cobra3d.png)
Performance comparison with various C compilers
-----------------------------------------------
Here is a performance comparison with various C compilers for the 6502/C64 so you can get
an idea of how Prog8 stacks up.
[comparison](benchmark-c/benchmark.md)

26
benchmark-c/benchmark.md Normal file
View File

@@ -0,0 +1,26 @@
# C-Bench-64 comparison
Several benchmarks of https://thred.github.io/c-bench-64/
have been ported to equivalent Prog8 code and the benchmark results have been penciled in in the graphs below.
Maybe one day I'll try to integrate the prog8 data properly but their benchmark site is comparing C compilers, which Prog8 clearly is not.
However conclusions so far (note: these are micro benchmarks so interpret the results as you will!)
* Prog8 program size is consistently the smallest by a fair bit.
* Prog8 execution speed places it more or less in the middle of the stack, with a regression in the Sieve benchmarks.
Measured with Prog8 V12.0 pre release.
![crc8](result-crc8.png)
![crc16](result-crc16.png)
![crc32](result-crc32.png)
![pow](result-pow.png)
![sieve](result-sieve.png)
![sieve-bit](result-sieve-bit.png)

71
benchmark-c/crc16.p8 Normal file
View File

@@ -0,0 +1,71 @@
%import textio
%import floats
; note: Prog8 has an optimized CRC16 routine in its library: math.crc16
main {
sub start() {
txt.lowercase()
test.benchmark_name()
cbm.SETTIM(0,0,0)
test.benchmark()
txt.print_f(floats.time() / 60)
txt.print(" seconds\n")
void test.benchmark_check()
repeat {}
}
}
test {
const uword EXPECTED = $ffd0
uword crc_result
sub benchmark_name()
{
txt.print("crc16.p8\n")
txt.print("Calculates the CRC16 of the C64 Kernal\n")
}
sub benchmark()
{
crc_result = CRC16($e000, $2000)
}
sub benchmark_check() -> bool
{
txt.print("CRC=")
txt.print_uwhex(crc_result, true)
if crc_result == EXPECTED
{
txt.print(" [OK]")
return false
}
txt.print(" [FAIL] - expected ")
txt.print_uwhex(EXPECTED, true)
return true
}
sub CRC16(^^ubyte data, uword length) -> uword {
; CRC-16/XMODEM
uword crc
repeat length
{
crc ^= mkword(@(data),0) ; currently there's no easy way to affect only the MSB of the variable
repeat 8
{
crc <<=1
if_cs
crc ^= $1021
}
data++
}
return crc
}
}

72
benchmark-c/crc32.p8 Normal file
View File

@@ -0,0 +1,72 @@
%import textio
%import floats
; note: Prog8 has an optimized CRC32 routine in its library: math.crc32
main {
sub start() {
txt.lowercase()
test.benchmark_name()
cbm.SETTIM(0,0,0)
test.benchmark()
txt.print_f(floats.time() / 60)
txt.print(" seconds\n")
void test.benchmark_check()
repeat {}
}
}
test {
const long EXPECTED = $e1fa84c6
long crc_result
sub benchmark_name()
{
txt.print("crc32.p8\n")
txt.print("Calculates the CRC32 of the C64 Kernal\n")
}
sub benchmark()
{
crc_result = CRC32($e000, $2000)
}
sub benchmark_check() -> bool
{
txt.print("CRC=")
txt.print_ulhex(crc_result, true)
if crc_result == EXPECTED
{
txt.print(" [OK]")
return false
}
txt.print(" [FAIL] - expected ")
txt.print_ulhex(EXPECTED, true)
return true
}
sub CRC32(^^ubyte data, uword length) -> long {
; CRC-32/CKSUM
long crc
repeat length
{
crc ^= (@(data) as long)<<24 ; currently there's no easy way to affect only the MSB of the variable
repeat 8
{
crc <<=1
if_cs
crc ^= $04c11db7
}
data++
}
crc ^= $ffffffff
return crc
}
}

72
benchmark-c/crc8.p8 Normal file
View File

@@ -0,0 +1,72 @@
%import textio
%import floats
main {
sub start() {
txt.lowercase()
test.benchmark_name()
cbm.SETTIM(0,0,0)
test.benchmark()
txt.print_f(floats.time() / 60)
txt.print(" seconds\n")
void test.benchmark_check()
repeat {}
}
}
test {
sub benchmark_name()
{
txt.print("crc8.p8\n")
txt.print("Calculates the CRC8 of the C64 Kernal\n")
}
sub benchmark()
{
crc_result = CRC8($e000, $2000)
}
sub benchmark_check() -> bool
{
txt.print("CRC=")
txt.print_ubhex(crc_result, true)
if crc_result == EXPECTED
{
txt.print(" [OK]")
return false
}
txt.print(" [FAIL] - expected ")
txt.print_ubhex(EXPECTED, true)
return true
}
const ubyte EXPECTED = $a2
ubyte crc_result
sub CRC8(^^ubyte data, uword length) -> ubyte
{
; CRC-8/GSM-A
ubyte crc
repeat length
{
crc ^= @(data)
repeat 8
{
if (crc & $80) != 0
crc = (crc << 1) ^ $1d
else
crc <<= 1
}
data++
}
return crc
}
}

93
benchmark-c/pow.p8 Normal file
View File

@@ -0,0 +1,93 @@
%import textio
%import floats
main {
sub start() {
txt.lowercase()
test.benchmark_name()
cbm.SETTIM(0,0,0)
test.benchmark()
txt.print_f(floats.time() / 60)
txt.print(" seconds\n")
void test.benchmark_check()
repeat {}
}
}
test {
const ubyte N_ITER = 10
const ubyte SIZE = 32
float[SIZE] array
const float expected = 3614007361536.000000
const float epsilon = 10000000
float res
sub benchmark_name()
{
txt.print("pow.p8\n")
txt.print("Calculates floating point exponential (")
txt.print_uw(N_ITER)
txt.print(" iterations)\n")
}
sub benchmark()
{
ubyte i,j
res = 0
for j in 0 to SIZE-1 {
array[j]=0
}
for i in 0 to N_ITER-1 {
for j in 0 to SIZE-1 {
array[j] += testpow(2.5 / ((i + 1) as float), j)
}
}
for j in 0 to SIZE-1 {
res += array[j]
}
}
sub testpow(float x, ubyte y) -> float
{
float tmp = x
if y == 0
return 1
repeat y-1
tmp *= x
return tmp
}
sub benchmark_check() -> bool
{
txt.print("res = ")
txt.print_f(res)
float diff = expected - res;
txt.print("\nexpected = ")
txt.print_f(expected)
txt.print("\nepsilon = ")
txt.print_f(epsilon)
txt.print("\ndiff = ")
txt.print_f(diff)
if (diff < epsilon and diff > -epsilon)
{
txt.print(" [OK]\n")
return false
}
txt.print("[FAIL]\n")
return true
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

BIN
benchmark-c/result-crc8.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

BIN
benchmark-c/result-pow.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

85
benchmark-c/sieve.p8 Normal file
View File

@@ -0,0 +1,85 @@
%import textio
%import floats
main {
sub start() {
txt.lowercase()
test.benchmark_name()
cbm.SETTIM(0,0,0)
test.benchmark()
txt.print_f(floats.time() / 60)
txt.print(" seconds\n")
void test.benchmark_check()
repeat {}
}
}
test {
const ubyte N_ITER = 10
const uword SIZE = 8191
const uword EXPECTED = 1900
uword prime_count
^^bool @zp flags = memory("flags", SIZE, 0)
sub benchmark_name()
{
txt.print("sieve.c\n")
txt.print("Calculates the primes from 1 to ")
txt.print_uw(SIZE * 2 + 2)
txt.print(" (")
txt.print_ub(N_ITER)
txt.print(" iterations)\n")
}
sub benchmark()
{
repeat N_ITER
prime_count = sieve(SIZE)
}
sub benchmark_check() -> bool
{
txt.print("count=")
txt.print_uw(prime_count)
if prime_count == EXPECTED
{
txt.print(" [OK]")
return false
}
txt.print(" [FAIL] - expected ")
txt.print_uw(EXPECTED)
return true
}
sub sieve(uword size) -> uword
{
uword i, prime, k
uword count = 1
for i in 0 to size-1
flags[i] = true
for i in 0 to size-1
{
if flags[i]
{
prime = i + i + 3
k = i + prime
while k < size
{
flags[k] = false
k += prime
}
count++
}
}
return count
}
}

107
benchmark-c/sieve_bit.p8 Normal file
View File

@@ -0,0 +1,107 @@
%import textio
%import floats
main {
sub start() {
txt.lowercase()
test.benchmark_name()
cbm.SETTIM(0,0,0)
test.benchmark()
txt.print_f(floats.time() / 60)
txt.print(" seconds\n")
void test.benchmark_check()
repeat {}
}
}
test {
const ubyte N_ITER = 4
const uword SIZE = 16000
const uword EXPECTED = 3432
uword prime_count
^^ubyte @zp flags = memory("flags", SIZE/8+1, 0)
sub benchmark_name()
{
txt.print("sieve_bit.p8\n")
txt.print("Calculates the primes from 1 to ")
txt.print_uw(SIZE * 2 + 2)
txt.print(" (")
txt.print_ub(N_ITER)
txt.print(" iterations)\n")
}
sub benchmark()
{
repeat N_ITER
prime_count = sieve(SIZE)
}
sub benchmark_check() -> bool
{
txt.print("count=")
txt.print_uw(prime_count)
if prime_count == EXPECTED
{
txt.print(" [OK]")
return false
}
txt.print(" [FAIL] - expected ")
txt.print_uw(EXPECTED)
return true
}
ubyte[] bitv = [
$01,
$02,
$04,
$08,
$10,
$20,
$40,
$80
]
sub check_flag(uword idx) -> bool
{
return flags[idx / 8] & bitv[lsb(idx) & 7] != 0
}
sub clear_flag(uword idx)
{
flags[idx / 8] &= ~(bitv[lsb(idx) & 7])
}
sub sieve(uword size) -> uword
{
uword i, prime, k
uword count=1
for i in 0 to (size / 8)
flags[i] = $ff
for i in 0 to size-1
{
if check_flag(i)
{
prime = i + i + 3
k = i + prime;
while k < size
{
clear_flag(k)
k += prime
}
count++
}
}
return count
}
}

View File

@@ -2533,7 +2533,11 @@ $endLabel""")
assignRegisterByte(target, CpuRegister.A, valueDt.isSigned, true)
return
}
in combinedLongRegisters -> TODO("assign byte to long reg ${value.position}")
in combinedLongRegisters -> {
assignExpressionToRegister(value, RegisterOrPair.A, false)
assignRegisterByte(target, CpuRegister.A, valueDt.isSigned, true)
return
}
else -> {}
}
} else if(valueDt.isUnsignedWord) {
@@ -2759,7 +2763,15 @@ $endLabel""")
else
asmgen.out(" lda $sourceAsmVarName | sta $targetAsmVarName | lda #0 | sta $targetAsmVarName+1")
}
BaseDataType.LONG -> TODO("assign typecasted to LONG")
BaseDataType.LONG -> {
asmgen.out("""
lda $sourceAsmVarName
sta $targetAsmVarName
lda #0
sta $targetAsmVarName+1
sta $targetAsmVarName+2
sta $targetAsmVarName+3""")
}
BaseDataType.FLOAT -> {
asmgen.out("""
lda #<$targetAsmVarName
@@ -3866,13 +3878,13 @@ $endLabel""")
}
}
internal fun assignRegisterByte(target: AsmAssignTarget, register: CpuRegister, signed: Boolean, extendWord: Boolean) {
internal fun assignRegisterByte(target: AsmAssignTarget, register: CpuRegister, signed: Boolean, extendSignedBits: Boolean) {
val assignAsWord = target.datatype.isWord
when(target.kind) {
TargetStorageKind.VARIABLE -> {
asmgen.out(" st${register.name.lowercase()} ${target.asmVarname}")
if(assignAsWord && extendWord) {
if(assignAsWord && extendSignedBits) {
if(target.datatype.isSigned) {
if(register!=CpuRegister.A)
asmgen.out(" t${register.name.lowercase()}a")
@@ -3901,7 +3913,7 @@ $endLabel""")
CpuRegister.X -> asmgen.out(" txa")
CpuRegister.Y -> asmgen.out(" tya")
}
if(extendWord) {
if(extendSignedBits) {
asmgen.signExtendAYlsb(if(target.datatype.isSigned) BaseDataType.BYTE else BaseDataType.UBYTE)
} else {
asmgen.out(" ldy #0")
@@ -3918,7 +3930,7 @@ $endLabel""")
RegisterOrPair.X -> { asmgen.out(" tax") }
RegisterOrPair.Y -> { asmgen.out(" tay") }
RegisterOrPair.AY -> {
require(extendWord) {
require(extendSignedBits) {
"no extend but byte target is registerpair"
}
if(signed)
@@ -3932,7 +3944,7 @@ $endLabel""")
asmgen.out(" ldy #0")
}
RegisterOrPair.AX -> {
require(extendWord)
require(extendSignedBits)
if(signed)
asmgen.out("""
ldx #0
@@ -3944,7 +3956,7 @@ $endLabel""")
asmgen.out(" ldx #0")
}
RegisterOrPair.XY -> {
require(extendWord)
require(extendSignedBits)
if(signed)
asmgen.out("""
tax
@@ -3960,10 +3972,16 @@ $endLabel""")
in Cx16VirtualRegisters -> {
val reg = "cx16.${target.register.toString().lowercase()}"
asmgen.out(" sta $reg")
if(extendWord)
if(extendSignedBits)
extendToMSBofVirtualReg(CpuRegister.A, reg, signed)
}
in combinedLongRegisters -> TODO("assign byte to long reg ${target.position}")
in combinedLongRegisters -> {
val reg = target.register.startregname()
asmgen.out(" sta cx16.$reg")
if(extendSignedBits) {
asmgen.signExtendLongVariable("cx16.$reg", if(signed) BaseDataType.BYTE else BaseDataType.UBYTE)
}
}
else -> throw AssemblyError("weird register")
}
CpuRegister.X -> when(target.register!!) {
@@ -3971,7 +3989,7 @@ $endLabel""")
RegisterOrPair.X -> { }
RegisterOrPair.Y -> { asmgen.out(" stx P8ZP_SCRATCH_REG | ldy P8ZP_SCRATCH_REG") }
RegisterOrPair.AY -> {
require(extendWord)
require(extendSignedBits)
if(signed)
asmgen.out("""
txa
@@ -3984,7 +4002,7 @@ $endLabel""")
asmgen.out(" txa | ldy #0")
}
RegisterOrPair.AX -> {
require(extendWord)
require(extendSignedBits)
if(signed)
asmgen.out("""
txa
@@ -3997,7 +4015,7 @@ $endLabel""")
asmgen.out(" txa | ldx #0")
}
RegisterOrPair.XY -> {
require(extendWord)
require(extendSignedBits)
if(signed)
asmgen.out("""
ldy #0
@@ -4012,7 +4030,7 @@ $endLabel""")
in Cx16VirtualRegisters -> {
val reg = "cx16.${target.register.toString().lowercase()}"
asmgen.out(" stx $reg")
if(extendWord)
if(extendSignedBits)
extendToMSBofVirtualReg(CpuRegister.X, reg, signed)
}
in combinedLongRegisters -> TODO("assign byte to long reg ${target.position}")
@@ -4023,7 +4041,7 @@ $endLabel""")
RegisterOrPair.X -> { asmgen.out(" sty P8ZP_SCRATCH_REG | ldx P8ZP_SCRATCH_REG") }
RegisterOrPair.Y -> { }
RegisterOrPair.AY -> {
require(extendWord)
require(extendSignedBits)
if(signed)
asmgen.out("""
tya
@@ -4036,7 +4054,7 @@ $endLabel""")
asmgen.out(" tya | ldy #0")
}
RegisterOrPair.AX -> {
require(extendWord)
require(extendSignedBits)
if(signed)
asmgen.out("""
tya
@@ -4049,7 +4067,7 @@ $endLabel""")
asmgen.out(" tya | ldx #0")
}
RegisterOrPair.XY -> {
require(extendWord)
require(extendSignedBits)
if(signed)
asmgen.out("""
tya
@@ -4066,7 +4084,7 @@ $endLabel""")
in Cx16VirtualRegisters -> {
val reg = "cx16.${target.register.toString().lowercase()}"
asmgen.out(" sty $reg")
if(extendWord)
if(extendSignedBits)
extendToMSBofVirtualReg(CpuRegister.Y, reg, signed)
}
in combinedLongRegisters -> TODO("assign byte to long reg ${target.position}")
@@ -4074,7 +4092,7 @@ $endLabel""")
}
}
}
TargetStorageKind.POINTER -> pointergen.assignByteReg(PtrTarget(target), register, signed, extendWord)
TargetStorageKind.POINTER -> pointergen.assignByteReg(PtrTarget(target), register, signed, extendSignedBits)
TargetStorageKind.VOID -> { /* do nothing */ }
}
}