18

The ZX Spectrum has two formats for storing numbers, both 40 bits, or five bytes.

  • The first is a floating point format, which consists of one exponent byte, and four mantissa bytes. The first bit of the mantissa is assumed to be 1 and so is not stored; this frees up one bit in the mantissa to store the sign (+/-).

  • The second is some kind of 16-bit integer format, fluffed up to fit the same amount of space as a float. This format consists of one zero byte, one 0xFF byte, two bytes containing the 16-bit integer, stored little-endian, and a dummy byte which I believe is ignored.

What is the motive for including this second format? Initially, I thought that the few things which are certainly integers, like a BASIC line number or valid arguments to POKE or AT and so on, those can be assumed to be integers so that arithmetic can be done more quickly. But then, the computer will have to check that the first two bytes equal 0xFF00 before proceeding to use the number. And that would incur a cost on any floating point calculation.

What if a mantissa happens to start with at least seven eight ones, and the sign happens to be negative, and the exponent happens to be zero? I see (the potential for) a bug there. That scenario would mean that the first two bytes of the float would be 0x00 and 0xff, so that could confuse the calculator routines.

2
  • 4
    I think the ZX81 is exactly the same in this regard, but not the ZX80. The ZX80 has an integer-only BASIC. Commented Jan 20, 2019 at 8:27
  • The second format consists of one zero byte, then one byte 0x00 or 0xFF to signal a positive or negative integer number, then the two bytes containing the 16-bit integer, stored little-endian, and a dummy 0x00 byte. For example: +10 == 00 00 0A 00 00 ; -10 == 00 FF F6 FF 00 ; 0 == 00 00 00 00 00 ; -65536 == 0 FF 00 00 00 ; Commented Oct 29, 2023 at 22:20

3 Answers 3

22

The ZX Spectrum has two formats for storing numbers, both 40 bits, or five bytes.

Jup, like many other machines - one for float and one for integer.

The second is some kind of 16-bit integer format,

Not some kind, but the integer format.

fluffed up to fit the same amount of space as a float.

Which was done to make variable memory management easy.

and a dummy byte which I believe is ignored.

But set to zero when an integer is stored

What is the motive for including this second format?

It's got several advantages

  • A one size fits all (numeric variables) approach, so memory management doesn't need to distinguish between integer and float

  • Integers are automatic detected, thus no need to declare integer variables like in other BASICs

  • Integers are always stored as integers

  • Calculation with integers can be sped up by avoiding conversion

  • Calculations with integers aren't harmed by being stored in float

[...] like a BASIC line number

I assume you mean for GO TO or alike, as line numbers themselves are handled (and stored) differently.

or valid arguments to POKE or AT and so on, those can be assumed to be integers so that arithmetic can be done more quickly.

Exactly.

But then, the computer will have to check that the first two bytes equal 0xFF00 before proceeding to use the number.

They are a marker. Which speeds up execution a lot. If numbers in like a POKE would be stored as float, then for each execution a float to integer conversion must be called (in fact twice; once for the byte to be poked as well). So instead conversion is only to be done if either marker byte is not present. A serious saving.

And that would incur a cost on any floating point calculation.

That's what? Two pairs of compare and jump, the second pair only executed at all for a tiny fraction of numbers? It's worth keeping in mind that a differentiation between FP or integer input from a variable has to be done anyway - so checking some flag stored at another position (where?) must happen - which makes the solution chosen almost free of cost in case of FP. Setup by a huge saving in not having to convert from FP if the number is to be used as integer anyway.

What if a mantissa happens to start with at least seven eight ones, and the sign happens to be negative, and the exponent happens to be zero? I see (the potential for) a bug there. That scenario would mean that the first two bytes of the float would be 0x00 and 0xff, so that could confuse the calculator routines.

No bug. It's a marker checked beforehand. Such a configuration is simply not an FP number. So Sinclair BASIC misses out of a tiny fraction of possible numbers compared to other BASICs with a similar encoding for FP. Not a big deal.

Especially not when it simplifies coding and speeds up execution as trade off.

P.S.: The whole details are covered in chapter 24 of the manual.


As there was a discussion about the impact of the introduction of this marker format on the range of FP numbers possible, be assured it's a rather small one.

The way it is implemented, it supports numbers from E-127 (Exponent encoded as 01h) to E+127 (FFh) (*1) without any restrictions. Without the integer marker it would go down to E-128. Sacrificing this for a clean integer handling in the same space and doing away with special integer variables is a cheap price to be payed.

Another idea that came up in discussion was the use of an appropriate exponent to simply use states where FP representation would match integer anyway. Like E+15, where the next two bytes would be exactly like the corresponding signed integer. Sounds cool at first, but it would end up in creating a mess.

For one, it only works with signed integer. Not a hurdle at first, as BASIC treats them that way anyway (*2), but latest when it comes to numbers smaller than +/-16384 representation would end up being not normalized - something that will screw floating point handling - unless additional checks are added, which in turn would slow down all FP calculation.

Next, and equally important, FP numbers in the range of integers that do have significant digits beyond the 15th (and thus being most definitive FP) would also identify as integer. To avoid this the trailing bytes (#4 and #5) must need to be checked for zero as well for safe identification as integer, making it 3 bytes to be tested instead of two, again slowing down identification.

Bottom line, the selected method is quite nice and solves its purpose at least as well as the more common MS-BASIC way of using variable identifiers (*3).


*1 - Exponents are encoded with high bit set for positive values to allow unsigned operations be used instead of signed. Or as the manuals put it " exponent +128 in the first byte".

*2 - Not sure ATM, but I think BASIC stores inline values as well in unsigned manner.

*3 - Depending on the way BASIC lines are stored, the the MS method of adding a type identifier (like %) may result in an additional byte per occurrence within the program, making integer variables even more costly.

7
  • The bytes in question are 0x00, and 0xFF. That's a zero exponent, not E-128, isn't it? Commented Jan 20, 2019 at 15:26
  • 1
    Ah, now I understand. Because in other words, if the first byte is zero, it's not an exponent. Commented Jan 20, 2019 at 15:36
  • That's what I meant with the term 'Marker'
    – Raffzahn
    Commented Jan 20, 2019 at 15:48
  • Implied-one floating-point formats can't handle the value zero without reserving at least one exponent value for things other than normaized values (since zero can't be represented as a normalized value). The only real "question" is whether to use that exponent only to represent the number zero, or allow it to indicate a variety of values (e.g. those that can be represented as a 16-bit integer) of which zero happens to be one.
    – supercat
    Commented Jan 21, 2019 at 21:15
  • 4
    So-called NaN Boxing is a technique still used in some programming languages today.
    – JdeBP
    Commented Jan 25, 2019 at 18:08
6

Doing integer arithmetic is by some orders of magnitude faster as floating point. So if there is a statement like

LET a = b + c

the interpreter would first check if both a and b are integers. If yes, then it can proceed with a simple 16-bit addition, like ADD HL, BC to get the result, and directly store it into c. Otherwise, it would have to do an expensive floating point addition.

3
  • Did the ZX series actually have different code for integer and fp math? MS BASIC, for instance, did not. Integer variables were converted to FP first, which made them even slower than FP variables. Commented Jan 22, 2019 at 15:40
  • Interesting question so I took a look at skoolkid.github.io/rom This exists skoolkid.github.io/rom/asm/2D3B.html - so yes basically. skoolkid.github.io/rom/asm/346E.html looks like a good place to start from.
    – cyborg
    Commented Apr 23, 2019 at 19:26
  • @MauryMarkowitz: yes it did. The assembler code is in the ZX Spectrum ROM Disassembly.
    – smci
    Commented May 22, 2020 at 4:57
6

I bought and read 'Complete (ZX) Spectrum ROM Disassembly' (Ian Logan, 1983, very out of print) many years ago, here are the answers, from my memory. (Btw, the ZX80 ROM is insanely tight Z80 assembler considering everything Sinclair jammed into ~13K of ROM (and still had to leave about 4K of the 16K for video buffer, IIRC). Tighter than the Commodore 64 or BBC Micro - with full in-memory support for a BASIC interpreter, and compare the number of BASIC keywords the Spectrum supported). Memory used to be very expensive too, so spending CPU cycles to save memory was a worthwhile tradeoff back then.

  • You always want to use integers wherever possible, for speed. (3.5MHz is not very fast at all, most opcodes take 2-5 cycles to execute, so you only have ~1.5M ops/sec and (e.g. in a game, with scrolling) you want to be refreshing the screen several frames/sec.) Moreover, unlike the Commodore 64, the Spectrum for cost reasons did not have a dedicated sprite chip (or a sound chip), so that loaded the CPU too.
  • Floating-point operations (+,-,*,/) used to take many more CPU cycles (hundreds) than integer operations. This was true on 8-bit, 16-bit and 32-bit CPUs throughout the 70s/80s/90s, up until the Intel 80386 when the FPUs got integrated. (This is because you have to compute the difference in exponents, check for underflow, denormalize, sign-extend (which used to be a loop with SLA/SRA shift operations), perform the actual operation (+/- OR *,/ which take longer, bit-serially), possibly normalize the result (another loop), and finally restore the sign bit and push back on the stack).
  • The Z80 couldn't afford to have super-expensive FP circuitry, so FP calculations had be done by software emulation, by the ROM. Slower.
  • Certain operations like integer division, SQRT, EXP, LN, LOG, LOG10, SIN, COS, TAN, their inverses (ACOS, ASIN, ATAN), and hyperbolic (SINH, COSH, TANH, ASINH, ACOSH, ATANH) will in general produce a float output, even if the input was integer. (Unless you used table lookup, fixed-point and approximations.)

Integer format:

  • The Spectrum uses twos' complement format for signed integers, which was and still is the standard format; also used by every other 8-bit CPU that I know of (6502, 6809). (There are only two possible formats really [1]: ones' complement and twos' complement, and twos' complement beats ones' complement for general-purpose use because you can directly add/subtract/multiply/divide integers, without having to waste an extra cycle or two first modifying them or sign-extending them. (That's why in subsequent years, RISC chips automatically do sign-extension to 32b when you load a signed 16b or 8b int))
  • "fluffed up to fit the same amount of space as a float". That's called "padded" to 5 bytes. The padding was in bytes 2 and 5, the 16b integer was in bytes 3,4. (The Spectrum actually didn't sign-extend them. But if they had wanted to do signed 32b arithmetic they would have.)
  • ([1] there are other possible formats for integers, like radix notation for computing residues in number-theory, or obsolete stuff like Binary-Coded Decimal for when we want to easily extract decimal digits from integers e.g. in a bank mainframe, but these aren't used for general-purpose integers).

Float format

  • It's trivial to distinguish a float from an integer just from their first byte (see Raffzahn's answer), since we know even a 32-bit integer will still have 8 bits of sign-extension.
  • The leading bit in FP is called the mantissa and it's always understood to be one (at least, it's always one in a normalized FP number). Hence we can reuse it to store the sign bit.
  • So the format is 1b sign, 31b mantissa, 8b exponent
  • This was still a few years before industry settled on IEEE 754 standard (1985), but it's similar, just with 31b mantissa instead of 23b. (I never heard a compelling reason why Sinclair chose 5 bytes instead of 4, but if I was to guess it would be to ensure good-enough accuracy throughout the domain of exponential and trigonometric functions, e.g. EXP, TAN, ATANH.)

What is the motive for including this second format? Initially, I thought that the few things which are certainly integers, like a BASIC line number or valid arguments to POKE or AT and so on, those can be assumed to be integers so that arithmetic can be done more quickly.

Hah! Welcome to the 1970s/80s/early 90s, where almost everything needed to be an integer, for the reasons mentioned above, programmers would try to keep many things integers: not just loop counters, spreadsheet values, but 2D and even much 3D game programming would use integers (possibly scaled, i.e. fixed-point) for speed in many cases where you might expect to see floating-point. If you want to see blazingly fast performant integer code used for graphics, see the series of books 'Graphics Gems' from the 1980s. See also id Games, programmers like John Carmack, Wolfenstein, Doom etc.

It's funny to see your assumption that every number should be a float, except for the "very few things that need to be an integer". Most quantities can be represented as integers outright (16b/32b/64b), or with fixed-point scaling. This is how computers worked for decades until integrating an FPU became cost-efficient Pentium, 1993 being the huge leap forward. If you looked under the hood at any list of great Spectrum games, even real-time 2D shooters, you'd see almost all integers; only 3D perspective games like Elite might maybe use floats [EDIT: no, Elite used integers too, presumably with fixed-point scaling; like Wolfenstein and id Games also did later].

But then, the computer will have to check that the first two bytes equal 0xFF00 before proceeding to use the number. And that would incur a cost on any floating point calculation.

As explained above, F-P calculations were already going to take tens or hundreds of cycles, so a couple of format-checking cycles had no impact. But saving lots of memory did.

What if a mantissa happens to start with at least seven eight ones, and the sign happens to be negative, and the exponent happens to be zero? I see (the potential for) a bug there. That scenario would mean that the first two bytes of the float would be 0x00 and 0xff, so that could confuse the calculator routines.

Yes that's the (very rare) special-case of a non-normalized floating-point. This only (slightly) limits us to the range of exponents we can handle; non-normalized numbers with exponents < -127 are very rare.

[I know almost nothing about the ZX80 or ZX81, except that they used the same Z80 family CPU, and the Spectrum ROM was adapted/extended from the ZX81's smaller ROM.

Footnote: Sinclair would have lived on in the transition to 16-bit personal computers if he had kept binary-compatibility, instead of his head-scratching 1984 decision to launch an overpriced, binary-incompatible Sinclair QL ('Quantum Leap'), based on an eccentric choice of Motorola 68008 as CPU, which meant his users' entire £££ investment in their library of 8-bit games, oops 'software', and hardware/peripherals/joysticks/keyboards/printers/modems was obsolete, in which case they had zero reason not to switch to the far more popular 16-bit Atari ST or Commodore Amiga, or the then-emerging PC or Macintosh, which was exactly the sad fate of the ZX series. But, it was a breakthrough for 1982, and introduced personal computing to a generation.]

3
  • 1
    A really great PDF of the Complete Spectrum ROM Disassembly is available: primrosebank.net/computers/zxspectrum/docs/… ; it appears not to be a scan but rather a team effort retyping, properly typeset.
    – Tommy
    Commented May 25, 2020 at 23:56
  • 1
    My understanding is that Elite also used integer arithmetic, with lookup tables to do any necessary trigonometry.
    – Neil
    Commented Jun 21, 2022 at 23:47
  • @Neil: A useful trick when using table-based trig is to exploit the fact that cos(d)cos(t) can be quickly approximated via (cos(t+d)+cos(t-d)). If an object is defined in polar coordinates, but using the arccosine of the radius instead of the radius, drawing the object in any rotation becomes absurdly easy.
    – supercat
    Commented Jun 22, 2022 at 23:32

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .