Tracks
/
x86-64 Assembly
x86-64 Assembly
/
Exercises
/
Freelancer Rates
Freelancer Rates

Freelancer Rates

Learning Exercise

Introduction

Floating-Point Numbers

A floating-point is a number which can take a fractional part, such as -1.23 or 3.1415.

In order to represent such a number as a sequence of bytes, x86-64 uses the IEEE-754 standard. The two most common formats in this standard are single-precision floating-point and double-precision floating-point numbers. x86-64 supports both.

A single-precision floating-point number is 32-bit wide, whereas a double-precision is 64-bit wide.

XMM Registers

In x86-64, there are 16 dedicated registers for doing floating-point arithmetic, named xmm0 to xmm15.

Those registers are at least 128-bit wide, but recent processors increase this to 256-bits or 512-bits. However, in practice, most floating-point numbers only use the lowest 32-bit or 64-bits, depending on the desired precision.

Conversion between numbers

There is a family of instructions for converting between number formats: cvtxx2yy.

Both xx and yy are not literals, but placeholders:

  • xx is a placeholder for the type of the source operand.
  • yy is a placeholder for the type of the destination operand.

This placeholder can take many forms, between them:

placeholder meaning
si signed integer
ss 32-bit floating-point
sd 64-bit floating-point

For example:

cvtsi2ss xmm0, eax ; converts a 32-bit signed integer to a single-precision floating-point
cvtsd2si rax, xmm1 ; converts a double-precision floating-point to a 64-bit signed integer

Note that it is possible to convert a 32-bit integer to a 64-bit floating-point or a 64-bit integer to a 32-bit floating-point. Similarly, it is also possible to convert a 32-bit floating-point to a 64-bit integer or a 64-bit floating-point to a 32-bit integer. In other words, the size of both operands in a cvtxx2yy do not need to be the same:

cvtsi2sd xmm0, eax ; converts a 32-bit signed integer to a double-precision floating-point
cvtsi2ss xmm0, rax ; converts a 64-bit signed integer to a single-precision floating-point
cvtss2si rax, xmm0 ; converts a single-precision floating-point to a 64-bit signed integer
cvtsd2si eax, xmm0 ; converts a double-precision floating-point to a 32-bit signed integer

When the floating-point number can not be exactly expressed as an integer, cvtxx2si rounds the number according to the rounding control in a special register called mxcsr. This could be a round to nearest, a round down, a round up or a truncate. There is no guaranteed default at function entry.

Note that a exact result is not rounded and, thus, not affected by the rounding control. For instance, 42.0 would always be converted to 42.

There is a similar family of instructions with the format cvttxx2si (with an extra t) that always truncates the result.

Round

The roundxx family of instruction converts a floating-point in the source operand to a rounded floating-point stored in the destination operand.

Here, xx is a placeholder for the type of the operands, as usual: ss for single-precision and sd for double-precision floats.

Those instructions take a constant number as third operand. This number indicates the rounding mode:

Number Mode
0 Nearest
1 Floor (down)
2 Ceil (up)
3 Truncate

For example:

roundss xmm0, xmm1, 1 ; this rounds down (takes the floor) a 32-bit floating-point in xmm1 and stores it in xmm0
roundsd xmm2, xmm2, 2 ; this rounds up (takes the ceil) a 64-bit floating-point in xmm2 and stores it in xmm2

Since those instructions produce an exact result, they can be used in preparation for a cvtxx2si.

Arithmetic Instructions

Many operations with floating-point numbers take instructions with a similar name as its equivalent for integers, but adding the necessary placeholder (ss or sd) at the end. Those instructions are two-operand, with the usual semantics, even those that perform multiplication or division:

addss xmm1, xmm2 ; xmm1 = xmm1 + xmm2 (all 32-bit floating-point)
mulsd xmm4, xmm5 ; xmm4 = xmm4 * xmm5 (all 64-bit floating-point)
divss xmm0, xmm3 ; xmm0 = xmm0 / xmm3 (all 32-bit floating-point)

Move

The instructions to move from a XMM register to another are movss or movsd:

movss xmm1, xmm0 ; this copies a 32-bit floating-point from xmm0 to xmm1
movsd xmm6, xmm4 ; this copies a 64-bit floating-point from xmm4 to xmm6

It is not possible to move an immediate (a constant signed integer) to a xmm register directly. This number first needs to be moved into a general-purpose register and then converted using cvtsi2xx:

mov rax, 10        ; rax is a 64-bit integer with value 10
cvtsi2sd xmm0, rax ; xmm0 is a 64-bit floating-point with value 10.0

Comparisons

There are two groups of instructions to make a comparison between two floating-point numbers. They take the form comixx and ucomixx, where xx is either ss or sd. Both set flags in rflags as usual, following the same semantics as cmp.

The difference between them is that ucomixx sets the parity flag (PF) if one of the two floating-point numbers has a NaN value, whereas comixx raises an exception in this case.

The comparison between floating-point values is unsigned:

ucomisd xmm0, xmm1
ja .greater         ; 'ja' must be used for "greater", not 'jg'
jb .lesser          ; 'jb' must be used for "lesser", not 'jl'

Calling Convention

In System V ABI, the first eight floating-point arguments are passed to functions in order, from xmm0 to xmm7. Floating-point values are returned from functions in xmm0 and, if necessary, xmm1.

All xmm registers are freely available to use, none of them is reserved for the caller.

Memory

Floating-point values can be defined in memory following the same semantics as integers of the same size. NASM (The Netwide Assembler - the assembler used by this track) also allows for declaration of floating-point values in scientific notation.

section .data
    pi dq 3.141592653589793 ; this is a double-precision floating-point value
    exp_num dd 1.e5 ; this is a single precision floating-point value equal to 1 * 10⁵
    exp_num2 dd 1.e+5 ; this is a single precision floating-point value equal to 1 * 10⁵
    frac dq 1.e-10 ; this is a double precision floating-point value equal to 1 * 10⁻¹⁰, i.e., 0.0000000001

Instructions

In this exercise, you'll be writing code to help a freelancer communicate with a project manager. Your task is to provide a few utilities to quickly calculate daily and monthly rates, optionally with a given discount.

We first establish a few rules between the freelancer and the project manager:

  • The daily rate is 8 times the hourly rate.
  • A month has 22 billable days.

Sometimes, the freelancer is offering to apply a discount on their daily rate (for example for their most loyal customers or not-for-profit customers).

Discounts are modeled as fractional numbers representing percentages, for example, 25.0 (25%).

Note

These are the suffixes to floating-point instructions:

Suffix Operand size
ss 32-bit
sd 64-bit

These are some of the instructions mentioned in this concept:

Instruction Description
movss/movsd a, b copies b to a
addss/addsd a, b a = a + b
subss/subsd a, b a = a - b
mulss/mulsd a, b a = a * b
divss/divsd a, b a = a / b
cvtsi2ss/cvtsi2sd a, b converts integer b to floating-point a
roundss/roundsd a, b, c rounds floating-point b according to c and stores in a
cvtss2si/cvtsd2si a, b converts floating-point b to integer a
comiss/comisd a, b makes a comparison between a and b, exception if NaN
ucomiss/ucomisd a, b makes a comparison between a and b, sets PF if NaN

These are the rounding control numbers in roundss and roundsd:

Number Mode
0 Nearest
1 Floor (down)
2 Ceil (up)
3 Truncate

1. Calculate the daily rate given an hourly rate

Implement a function called daily_rate to calculate the daily rate given as parameter an hourly rate, as a 64-bit floating-point number. The contract defines that a day has 8 billable hours.

daily_rate(60)
// => 480.0

The returned daily rate should be a 64-bit floating-point number.

2. Calculate a discounted price

Implement a function apply_discount to calculate the price after a discount. It should accept two parameters: the original price and the discount rate in percent, both as a 64-bit floating-point number.

apply_discount(150.0, 10.0)
// => 135.0

The returned value should always be a 64-bit floating-point number.

3. Calculate the monthly rate, given an hourly rate and a discount

Implement a monthly_rate function to calculate the discounted monthly rate. It takes two parameters, an hourly rate and the discount in percent, both as a 64-bit floating-point number.

monthly_rate(77.0, 10.5)
// => 12130

The returned monthly rate should be rounded up (take the ceiling) to the nearest 64-bit integer.

4. Calculate the number of complete workdays given a budget, hourly rate, and discount

Implement a function days_in_budget that takes as parameters:

  1. A budget, as a 64-bit unsigned integer.
  2. An hourly rate, as a 64-bit floating-point number.
  3. A discount in percent, as a 64-bit floating-point number.

This function should return the number of complete days of work the budget covers.

days_in_budget(20000, 80, 11.0)
// => 35

The returned number of days should be rounded down (take the floor) to the nearest 32-bit integer.

Edit via GitHub The link opens in a new window or tab
x86-64 Assembly Exercism

Ready to start Freelancer Rates?

Sign up to Exercism to learn and master x86-64 Assembly with 22 concepts130 exercises, and real human mentoring, all for free.