AArch64 Playground
4.28 · Floating point: the other register file

Floating point: the other register file

An integer register cannot hold 12.5 or 0.045. For numbers with a fraction the processor has floating-point numbers: a value stored as a sign, a power of two, and a fixed number of significant digits, the way scientific notation writes 6.02 * 10^23, but in binary. How those bits are laid out is the subject of Inside a float: IEEE 754; this lesson is about using them.

Floating-point values live in their own set of 32 registers, their own register file, apart from x0 to x30, and have their own instructions, nearly all of them starting with f.

The s and d registers

Each of the 32 floating-point registers can be read at more than one width. d0 to d31 are 64 bits wide and hold a double (double precision, C's double, good for about 15 significant decimal digits). s0 to s31 are the low 32 bits of the same registers and hold a single (single precision, C's float, about 7 digits). d3 and s3 are one register seen two ways, just as x3 and w3 are, and writing s3 clears the rest of it. The full 128-bit register is called v3; the instructions that work on several values at once use that name, and this lesson needs only s and d.

An instruction works at one width. fadd d0, d1, d2 adds doubles and fadd s0, s1, s2 adds singles. fadd d0, s1, d2 does not assemble: convert one operand first, as shown further down.

Constants

A floating-point constant goes in .data with .double (8 bytes) or .float (4 bytes). The number is written with a 0r prefix, which marks it as a real number rather than an integer: rate: .double 0r0.045. Load it the way you load any variable, its address first and then the value, but into a d or s register: ldr x9, =rate and then ldr d9, [x9].

fmov can put a constant straight into a register, but only from a small set: a whole number of sixteenths times a power of two, from 0.125 up to 31, such as 0.25, 0.5, 1.0, 2.5, and 10.0. fmov d1, 0.1 does not assemble, because no such product equals 0.1 exactly; put 0.1 in .data instead. fmov also copies one register to another (fmov d0, d8) and copies raw bits from a general register: fmov d8, xzr makes 0.0, because a double whose 64 bits are all zero is 0.0.

Arithmetic

fadd, fsub, fmul, and fdiv take a destination and two sources, like their integer cousins. A few more have no integer twin:

  • fmadd d0, d1, d2, d3 computes d3 + d1 * d2, and fmsub d0, d1, d2, d3 computes d3 - d1 * d2. Each rounds once, at the end, instead of after the multiply and again after the add.
  • fnmul d0, d1, d2 is -(d1 * d2).
  • fabs and fneg take one source and give its absolute value and its negation.

Dividing by zero does not stop the program. 1.0 / 0.0 gives infinity, and 0.0 / 0.0 gives NaN, a special value meaning "not a number"; printf shows them as inf and nan.

Printing with %f

printf reads each %f from the floating-point registers: the first from d0, the next from d1, and so on. The format string still goes in x0 and each %d still comes from w1, w2, and on, because the two kinds of argument are counted separately. The format "year %2d: interest %6.2f, balance %8.2f\n" takes the year from w1, the interest from d0, and the balance from d1. %.2f prints two digits after the point, and %8.2f also pads the number to 8 characters so the columns line up.

The program below grows a balance of 1000.00 by 4.5 percent a year. Each pass multiplies the balance by the rate to get the year's interest, adds the interest on, and prints both. The balance and the rate have to survive every call to printf, so they live in d8 and d9, two registers that a call must leave as it found them; the rules are in the last section.

loading editor...

regfile

N clearZ clearC clearV clear

x0–x30 are the integer registers.

X0arg00x0000000000000000
X1arg10x0000000000000000
X2arg20x0000000000000000
X3arg30x0000000000000000
X4arg40x0000000000000000
X5arg50x0000000000000000
X6arg60x0000000000000000
X7arg70x0000000000000000
X8ind0x0000000000000000
X90x0000000000000000
X100x0000000000000000
X110x0000000000000000
X120x0000000000000000
X130x0000000000000000
X140x0000000000000000
X150x0000000000000000
X16ip00x0000000000000000
X17ip10x0000000000000000
X18pr0x0000000000000000
X190x0000000000000000
X200x0000000000000000
X210x0000000000000000
X220x0000000000000000
X230x0000000000000000
X240x0000000000000000
X250x0000000000000000
X260x0000000000000000
X270x0000000000000000
X280x0000000000000000
X29fp0x0000000000000000
X30lr0x0000000000000000
SP0x0000000080000000
PC0x0000000000400000
console

Output prints here as your program runs.

Press step or run under the editor, or feed stdin from the box below.

not assembled

example 1try it: run it, or step one instruction at a timeOpen in playground

It prints ten lines, from year 1: interest 45.00, balance 1045.00 to year 10: interest 66.87, balance 1552.97.

note

Look at year 2: 1045.00 plus 47.02 is 1092.02, yet the line says 1092.03. The machine kept every digit it could. The interest was a hair under 47.025 and the new balance a hair over 1092.025, and printf rounded each one on its own as it printed it, the first down and the second up. Rounding happens only in what you print, never in what the register holds.

Printing a single

%f always means a double. printf is variadic: it takes any number of arguments after the format string. C widens every float it passes to a variadic function into a double, so printf never receives a single at all. Widen one yourself with fcvt d0, s0 before the call. Skip that step and printf reads the 32 bits of s0 as if they were a 64-bit double, a number so tiny it prints as 0.00.

Converting between integers and floating point

  • scvtf d1, w9 turns a signed integer into a double, so 100 becomes 100.0 (ucvtf does the same for an unsigned integer).
  • fcvtzs w2, d0 turns a double into a signed integer by dropping the fraction, as C's (int)x does: 23.7 becomes 23 and -23.7 becomes -23.
  • fcvtns w1, d0 rounds to the nearest integer instead: 23.7 becomes 24. A value exactly halfway goes to the even neighbor, so 2.5 becomes 2 and 3.5 becomes 4 (fcvtnu rounds the same way to an unsigned result).
  • fcvt d0, s0 widens a single to a double, and fcvt s0, d0 narrows a double to a single, rounding off the digits that do not fit.

Comparing

fcmp d1, d2 compares two values of the same width and sets the flags, and the signed conditions read them just as they do after cmp: b.gt, b.ge, b.lt, b.le, b.eq, and b.ne. Most results carry a little rounding, so compare against a limit with b.gt or b.le rather than testing for an exact match with b.eq.

The program below shows each conversion. It divides 100 by 8 twice, once with sdiv and once with fdiv after scvtf, then uses fcmp to pick the warmer of two single-precision sensor readings, rounds it both ways, and widens it with fcvt so printf can print it.

loading editor...

regfile

N clearZ clearC clearV clear

x0–x30 are the integer registers.

X0arg00x0000000000000000
X1arg10x0000000000000000
X2arg20x0000000000000000
X3arg30x0000000000000000
X4arg40x0000000000000000
X5arg50x0000000000000000
X6arg60x0000000000000000
X7arg70x0000000000000000
X8ind0x0000000000000000
X90x0000000000000000
X100x0000000000000000
X110x0000000000000000
X120x0000000000000000
X130x0000000000000000
X140x0000000000000000
X150x0000000000000000
X16ip00x0000000000000000
X17ip10x0000000000000000
X18pr0x0000000000000000
X190x0000000000000000
X200x0000000000000000
X210x0000000000000000
X220x0000000000000000
X230x0000000000000000
X240x0000000000000000
X250x0000000000000000
X260x0000000000000000
X270x0000000000000000
X280x0000000000000000
X29fp0x0000000000000000
X30lr0x0000000000000000
SP0x0000000080000000
PC0x0000000000400000
console

Output prints here as your program runs.

Press step or run under the editor, or feed stdin from the box below.

not assembled

example 2try it: run it, or step one instruction at a timeOpen in playground

It prints three lines: sdiv answers 12, fdiv answers 12.50, and the warmer reading, 23.70, becomes 24 with fcvtns and 23 with fcvtzs.

Floating point and subroutines

The calling convention (the rules every function follows) has a floating-point half that mirrors the integer half:

  • Arguments go in d0 to d7 (s0 to s7 for singles), counted separately from the integer arguments in x0 to x7. A function scale(int count, double factor) receives count in w0 and factor in d0.
  • A floating-point result comes back in d0 (or s0).
  • d8 to d15 are callee-saved: a function that changes one must put the old value back before it returns, just like x19 to x28. Only their low 64 bits count, so saving the d register is enough.
  • Every other floating-point register, d0 to d7 and d16 to d31, may be changed by any call, printf included.

The next program turns the formula a + (b - a) * t into a subroutine. This linear interpolation finds the point a fraction t of the way from a to b: t = 0 gives a, t = 1 gives b, and t = 0.5 the point halfway. lerp takes a, b, and t in d0, d1, and d2, does the work with one fsub and one fmadd, and returns the answer in d0. main walks t from 0 to 1 in steps of 0.25 and keeps it in d8 so that it survives the calls. Because d8 is callee-saved, main stores the old d8 in its frame first and loads it back before it returns.

loading editor...

regfile

N clearZ clearC clearV clear

x0–x30 are the integer registers.

X0arg00x0000000000000000
X1arg10x0000000000000000
X2arg20x0000000000000000
X3arg30x0000000000000000
X4arg40x0000000000000000
X5arg50x0000000000000000
X6arg60x0000000000000000
X7arg70x0000000000000000
X8ind0x0000000000000000
X90x0000000000000000
X100x0000000000000000
X110x0000000000000000
X120x0000000000000000
X130x0000000000000000
X140x0000000000000000
X150x0000000000000000
X16ip00x0000000000000000
X17ip10x0000000000000000
X18pr0x0000000000000000
X190x0000000000000000
X200x0000000000000000
X210x0000000000000000
X220x0000000000000000
X230x0000000000000000
X240x0000000000000000
X250x0000000000000000
X260x0000000000000000
X270x0000000000000000
X280x0000000000000000
X29fp0x0000000000000000
X30lr0x0000000000000000
SP0x0000000080000000
PC0x0000000000400000
console

Output prints here as your program runs.

Press step or run under the editor, or feed stdin from the box below.

not assembled

example 3try it: run it, or step one instruction at a timeOpen in playground

It prints five lines, one for each quarter: 20.0, 60.0, 100.0, 140.0, and 180.0 degrees.

pitfall

The step of 0.25 and the limit of 1.0 go through d16, which any call may change. That is why the loop sets d16 again just before each use, and never counts on it across bl lerp or bl printf. A value that must outlive a call belongs in d8 to d15, saved in the prologue (the function's opening lines), or in memory.

pitfall

Common mistakes from this lesson, each with a broken program and its fix that you can run:

Check yourself

  1. Which register holds the format string for printf, and which holds the first %f?
  2. A function takes (int count, double scale). Where does each argument arrive?
  3. Why does a single need fcvt d0, s0 before it is printed?
  4. A loop keeps a running total in d5 and calls printf on every pass. What goes wrong, and what is the fix?
  5. fmov d1, 0.1 does not assemble. What do you write instead?

answers

show answers
  1. x0 holds the format string and d0 the first %f.
  2. count in w0 and scale in d0: integer and floating-point arguments are counted separately.
  3. printf only ever receives doubles, because C widens a float passed to it, so %f reads 64 bits.
  4. printf may change d0 to d7, so the total can be lost. Keep it in one of d8 to d15 and save that register in the prologue.
  5. Put .double 0r0.1 in .data under a label, then load its address and ldr d1 from it.

Practice