20

Since numbers in ZX BASIC are always floating point and take up 5 bytes, it's often possible to save space when representing integers by using several tokens, which take up 1 byte each.

I know of COS PI for -1, NOT PI for 0, SGN PI for 1, INT EXP SGN PI for 2, and INT PI for 3.

I can also think of a few like INT EXP PI would be 23. Is there a complete list of combinations of tokens which are shorter than regular integer representations, up to (say) integer 63?

Related: Why does this BASIC program declare variables for the numbers 0 to 4?

8
  • 6
    @leftaroundabout yes, it is. I's more SPACE efficient - see the linked questions for that :))
    – Raffzahn
    Jun 16, 2021 at 11:20
  • 4
    @Flydog57, well, that sounds a bit harsh based on only that information. I mean, the story as you tell it doesn't really explain why exactly they did that. I mean, if it's something that can really use speed, it might make sense. Provided of course you take care to ensure the results are correct and you get a net gain in the end instead of e.g. getting swamped with moving the data around if you need to do arithmetic on array indexes or pointers or so. And you'd need to make it autoconfigurable for serious use, but could drop that in a hobby project.
    – ilkkachu
    Jun 17, 2021 at 13:18
  • 4
    @Flydog57, But it's not like floating point automatically means inexact results, e.g. 64-bit doubles still have 53 bits of mantissa, so can deal with rather large integers exactly.
    – ilkkachu
    Jun 17, 2021 at 13:20
  • 4
    This wasn't a hobby project; it was a large scale commercial product. That wasn't the only reason he was a no hire, but the whole discussion we had with the candidate around that decision helped put him in the no-hire column (not that we were arguing about what some other company should do, we were discussing why you would spend money to do something like that, what were the testing implications, etc.). In those days, Moore's law was everywhere. Whenever a new chip was release, it was faster than the last one. Basing a major decision on something like that seemed pretty short-sighted
    – Flydog57
    Jun 17, 2021 at 14:07
  • 5
    @Flydog57 Your reasoning sounds quite valid. Except, the start is a bit off, as this is about a BASIC on a computer with 16 KiB of Memory (well, 1 in case of a ZX81). As soon as programs leave the 10 PRINT "HELLO" region, space gets an absolute premium, so space saving measures, even if they look weird, are necessary. And the ones mentioned are rather standard, so well understood by average programmers of that machine.
    – Raffzahn
    Jun 17, 2021 at 14:38

3 Answers 3

24

Solution:

I know, this is a bit unfair, but there's a generic solution to that:

  • VAL "n"

Using VAL with an integer(*1) will always be three bytes shorter than that integer used directly. No matter if needed in an expression, as parameter or as a GO TO/ GO SUB target :)

That's right, GOTO VAL "10" is 3 bytes shorter than GOTO 10 :)

How it Works:

As described here

  • a numeric constant uses 6 bytes plus its ASCII representation.
  • VAL "" takes 3 bytes plus the ASCII string to be converted.

So there's always a net saving of 3 bytes.

The Fine Print

Of course doing an ASCII conversion every time a constant kills performance - essentially negating the otherwise great benefit of Sinclair BASIC regarding constants. Thus, in next to all real world cases, it is recommended to store it in a variable instead of doing that conversion every time. This not only speeds up execution, gaining back a bit of the lost speed, but saves another 2-3 bytes per usage (*2) after the second usage(*3).


*1 - Well, the same is of course true for float as well.

*2 - 2 bytes minimum with a single digit integer used and a two character variable. 3 bytes with a 1 character variable. and an additional byte for each additional digit of the number in question - all per usage.

*3 - Assigning a variable has an overhead of 5 bytes, so it need to be used at least twice to save over all.

4
  • 1
    So, if you created a program file that contained a numeric constant where the binary and ASCII representation didn't match, what would happen? Would it display one value when printing the program, but use the other one when running it...?
    – ilkkachu
    Jun 16, 2021 at 21:34
  • 1
    @ilkkachu To my understanding: Yes. When listing the ASCII part is shown, when running the binary part is used. A nifty way for protection against modification - or a way to 'save' even more memory.
    – Raffzahn
    Jun 16, 2021 at 23:01
  • 5
    @ilkkachu: Yes, that's exactly what happened. The game 'Spellbound' did this in its BASIC loader; the command to launch the game printed as RANDOMIZE USR 26624 but the stored constant was 26627. 26624 displayed a message HELLO HACKER - FANCY MEETING YOU HERE! while 26627 was the entry point for the game. If the loader was edited (commonly done when adding cheats to a game), the stored constant would be updated to 26624, so instead of the game starting the HELLO HACKER message would be displayed.
    – john_e
    Jun 17, 2021 at 8:00
  • 1
    @john_e, yep, something like that crossed my mind too
    – ilkkachu
    Jun 17, 2021 at 9:54
7

There are quite a few options available if you need to reduce the size of your BASIC programme. Some of the options are more extreme and may require binary modification of your program (or the use of external tools/programs). Therefore, I'll try to document both options available to you as a vanilla BASIC programmer, and options that will require binary editing.

  1. Numbers are stored in ZX BASIC with two representations, simultaneously. The "user" representation is literally a string that you typed. The "internal" representation is always 6 bytes long and it contains special code 14 followed by 5-byte internal format (it is true for both floating point and integer numbers). Thus, usually, an N-digit number will occupy N+6 bytes of memory. This is the upper bound for people who work in vanilla BASIC.

For example, RANDOMIZE USR 49152 occupies 2+5+6 = 13 bytes of memory.

  1. The internal representation is used for all computations and is computed when the string is inputted. A common hack involves typing in the program using 1-digit numbers for all numerical constants (e.g. "0") and then replacing internal representations for these constants by the required values. This is awkward to do directly on ZX Spectrum, but with an external hex editor it can be done routinely enough. This means that ANY number (including floating point) can be stored using no more than 7 bytes of memory, this is the upper bound for people who are pushing their BASIC to its absolute limit.

If you see a protected program doing RANDOMIZE USR 0, it is not very likely to jump to address 0; in fact, it is likely to have been modified to jump to an address you can figure out by looking at the internal representation.

  1. Both Raffzahn and I covered the use of VAL before, but I'll include it here too, for the sake of completeness. Briefly, any number written as VAL "number" does not store an internal representation. So VAL "0" is only 4 bytes long, and, more generally, when using VAL, N-digit number would only occupy N+3 bytes of memory.

Many loaders begin with a command like CLEAR VAL "24999". It occupies 1+5+3 = 9 bytes of memory. A seemingly shorter command CLEAR 24999 would have occupied 2+5+6 = 13 bytes of memory instead.

  1. Note that if we allow binary editing, CLEAR 0, with an internal representation of 0 modified to represent 24999, would only occupy 8 bytes. Therefore, VAL is particularly competitive for shorter numbers, no longer than 3 digits. In fact, I should have said "characters", not "digits". It is because many numbers can be written a bit more compactly using the scientific notation, or using mathematical operations within a VAL expression string. For example, don't write "25000", write "25E3", don't write VAL "32768", write VAL "2^15" or, even better, VAL "8^5", etc.

  2. For integer numbers that fit into a byte, you also have an option of using function CODE that returns ZX ASCII code of the character provided. Similarly to VAL "0", CODE "0" occupies 4 bytes. However, CODE "." can generate two- and some three-digit numbers, which would require a longer VAL command. Thus, CODE is useful to represent numbers from 10 to 255 using 4 bytes of memory. Most specific codes can be entered directly from BASIC, but the full range 0..255 would require binary editing of your program.

A popular trick to remove any output from the loading command is to POKE 23739,111. This command occupies 1+5+6+3+6 = 21 bytes. Most actual loaders would POKE VAL "23739",CODE "o", which occupies only 1+5+3+4 = 13 bytes instead, the saving of over 30%.

  1. Last, but not least, there are also tricks with various funny expressions, some of which you mentioned in your original question. Below, I'm setting up a table of tricks that I know and I would be happy to add any futher tricks if someone is happy to share more. Please note that many BASIC commands that expect integer inputs would automatically round the numbers that are not integer, so these approximate inputs can be useful even if they do not produce exact integer values:
   Number     Best exact expression     Shortest approximate expression

      -1         COS PI (2 bytes)
       0         NOT PI (2 bytes)
       1         SGN PI (2 bytes)
       2         VAL "2" (4 bytes)             SQR PI (2 bytes)
       3         INT PI (2 bytes)              PI (1 byte)
       4         VAL "4" (4 bytes)             EXP ATN PI (3 bytes)
       5         VAL "5" (4 bytes)             SQR EXP PI (3 bytes)
       6         VAL "6" (4 bytes)             PI+PI or EXP SQR PI (3 bytes)
       7         VAL "7" (4 bytes)
       8         VAL "8" (4 bytes)
       9         LEN STR$ PI (3 bytes)
      10         VAL "10" (5 bytes)            PI*PI (3 bytes)
   16384         VAL "4^7" (6 bytes)
   16807         VAL "7^5" (6 bytes)
   19683         VAL "3^9" (6 bytes)
   20000         VAL "2E4" (6 bytes)
   30000         VAL "3E4" (6 bytes)
   32768         VAL "8^5" (6 bytes)
   40000         VAL "4E4" (6 bytes)
   46656         VAL "6^6" (6 bytes)
   50000         VAL "5E4" (6 bytes)
   59049         VAL "9^5" (6 bytes)
   60000         VAL "6E4" (6 bytes)
4
  • Wouldn't PI*PI work for 9 ? Jun 17, 2021 at 15:01
  • 1
    It would be equal to 9.87, so the commands expecting an integer would round it up to 10. However, here I focused on values 0..7 - the only possible colour values on ZX Spectrum.
    – introspec
    Jun 17, 2021 at 20:53
  • I wonder how much more ROM would have been required to detect numbers in the range 0-65535 were written without leading zeroes, and have a token indicating that the following two bytes are a 16-bit integer whose value should be output as part of the source code?
    – supercat
    Jul 22, 2022 at 17:18
  • @supercat, there is about 1K of unused space in the 16/48K ZX Spectrum ROM. It would have been even easier to add a special code for internal representation of integers, saving 3 bytes on every one of them. However, 1) ROM was not finished, its authors left before the work was complete and 2) I don't think saving program RAM was their priority, see computinghistory.org.uk/det/51620/…
    – introspec
    Jul 23, 2022 at 20:42
3

I remember seeing PI/PI instead of 1 in the programs back then a lot. Clearly they did not know a better trick.

2

You must log in to answer this question.

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