ARM immediate value encoding
alisdair.mcdiarmid.org
alisdair.mcdiarmid.org
For those interested, check out page 122 of the ARMv7-M architecture reference manual[2]:
// ThumbExpandImm_C()
// ==================
(bits(32), bit) ThumbExpandImm_C(bits(12) imm12, bit carry_in)
if imm12<11:10> == ’00’ then
case imm12<9:8> of
when ’00’
imm32 = ZeroExtend(imm12<7:0>, 32);
when ’01’
if imm12<7:0> == ’00000000’ then UNPREDICTABLE;
imm32 = ’00000000’ : imm12<7:0> : ’00000000’ : imm12<7:0>;
when ’10’
if imm12<7:0> == ’00000000’ then UNPREDICTABLE;
imm32 = imm12<7:0> : ’00000000’ : imm12<7:0> : ’00000000’;
when ’11’
if imm12<7:0> == ’00000000’ then UNPREDICTABLE;
imm32 = imm12<7:0> : imm12<7:0> : imm12<7:0> : imm12<7:0>;
carry_out = carry_in;
else
unrotated_value = ZeroExtend(’1’:imm12<6:0>, 32);
(imm32, carry_out) = ROR_C(unrotated_value, UInt(imm12<11:7>));
return (imm32, carry_out)
[1] http://graphics.stanford.edu/~seander/bithacks.html (worth a read on its own if you're into this kind of thing)[2] http://web.eecs.umich.edu/~prabal/teaching/eecs373-f10/readi... (no-registration link)
0x3f800000 // encoding of 1.0f
The set of immediate encodings, together with "shifts for free on most operations" (which are closely related features, as the OP points out), went a long way toward preserving my sanity when writing assembly.Worth noting: thumb-2 immediates have a different (and even more interesting) encoding scheme. arm64 immediates are pretty interesting too (there the set of representable immediates is different depending on the instruction domain).
But then, this way you have a nice set of imediates (as you said), and can set any value at all with at most 3 instructions at the rare case you need something different.
There's one ISA I've worked with before that does have -2, -1, 0, 1, 2, and some other "commonly used constants" like powers of 2 encoded specially in the immediate. I think it was an 8-bit, but I can't remember exactly which one. Anyone know what I'm referring to?
Code size reductions are good, but for power purposes it is a case of balancing them against decoding complexity.
Interestingly, the website uses svg for illustrations, IE 8 and under be damned.
"Almost all ARM instructions can include an optional condition code. An instruction with a condition code is only executed if the condition code flags in the CPSR meet the specified condition. The condition codes that you can use are shown in Table 4.2."
f.e. execute this instruction only if the previous instruction resulted in a negative number
In ARM state, all instructions are conditionally executed according to the state of the CPSR condition codes and the instruction’s condition field. This field (bits 31:28) determines the circumstances under which an instruction is to be executed. If the state of the C, N, Z and V flags fulfils the conditions encoded by the field, the instruction is executed, otherwise it is ignored.
When condition is set the mnemonic of instruction is extended with one of suffixes like EQ, NE, CS, CC etc.
As a simple contrived example, consider the following C code:
int a[100], b[100], count;
...
for (int i=0; i<100; i++) {
if (a[i] > b[i]) count++;
}
without conditional execution, one might compile this to code that uses a branch to either increment count or not; on ARM it would be more idiomatic to use conditional execution. Here's a very literal translation as an example (not tested, apologies for any inadvertent errors): // setup: a in R0, b in R1, count in R2, i in R3.
loop: LDR R4, [R0, R3, LSL #2] // load a[i]
LDR R5, [R1, R3, LSL #2] // load b[i]
CMP R4, R5 // if a[i] > b[i]
ADDGT R2, R2, #1 // count++
ADD R3, R3, #1 // i++
CMP R3, #100 // if (i < 100)
BLT loop // continue loop
The fourth instruction, ADDGT, is conditional. Count is only updated with the result of the addition if the "greater than" condition is satisfied (the flags were set by the preceding instruction). To be more precise, all of the instructions here are conditional, it's just that for most of them the condition field is 1110, meaning "always".Many instructions also have an "S" bit, which toggles whether or not they update the flags on which conditional execution depends. Taken together, these two features allow a clever assembly programmer to do some really clever things (but historically not too much effort has been directed at getting compilers to make really clever use of these features).
For low-power parts, this is a cute trick, as it allows a programmer to avoid stressing a limited branch predictor with lots of small branches. It does add some complication to the implementation however, especially when you get into designs that retire multiple instructions per cycle or support out-of-order execution, as conditional execution basically adds additional dependencies to every instruction.
That said, it also means debugging becomes a bit more painful. Let's say you want your (cheap) JTAG debugger to halt on the count++ instruction. You can hard break on that particular address in code, but you will always hit that address whether the condition was met or not.
I'm new to the assembly world, I've been working my way down and have gotten as far as Forth.
So, a neat idea, but not for the long term, and certainly not for all CPUs. For example, ARM 64 ditches this feature (https://www.mikeash.com/pyblog/friday-qa-2013-09-27-arm64-an...)
To the usual complement of typical conditional instructions (branch, add/sub with carry, select and set), arm64 adds select with increment, negate, or inversion, the ability to conditionally set to -1 as well as +1, and the ability to conditionally compare and merge the flags in a fairly flexible manner (it’s really a conditional select of condition flags between the result of a comparison and an immediate). This actually preserves most of the power of conditional execution (except for really exotic hand-coded usages), while taking up much less encoding space.
1. load them from a constant pool in memory (often this is put nearly inline in the instruction stream so that it can be addressed via a small constant offset from PC). If your code doesn't saturate the LSU locally, this is usually the best option.
2. assemble them via bitwise or arithmetic combinations of literals that you can represent (or already in-register values that you know something about). If you can do it with just a few operations, and the LSU is otherwise occupied, you should do this instead. This is also preferred on some limited cores that can dual-issue logical and arithmetic ops but not LSU ops.
3. in recent revisions of the instruction set, there are a pair of instructions MOVW and MOVT; MOVW conjures a 16b immediate, and MOVT sets the high 16b, so with these two instructions you can conjure any 32b value.
There is a bit of subtlety to choosing the correct approach. Some assemblers provide a "conjure this value" pseudo-operation where the assembler will choose what it thinks is the best option to materialize a constant; that way the programmer doesn't need to worry about these details. This looks something like the following:
LDR R0, =0xff0000ff
the assembler might actually stash the constant somewhere and generate a load instruction, or it might instead do: MOV R0, 0xff000000
ORR R0, R0, 0x000000ff
and assemble the value via a couple instructions with immediates. add_something: ; function starts here (argument r0 == some number)
ldr r1, __tmp ; get complicated constant, store in r1
add r0, r0, r1 ; do the addition r0 = r0 + r1
bx lr ; == return result (in r0)
__tmp:
.word 0x12345678 ; store complicated constant here
You can play with your compiler, if you call gcc as "gcc -Os -S -o- file.c" if will spit out generated assembler code (-S) on stdout "-o-" for the c-code in file.c.(but then, gcc prefers to have 4 "compact" adds, instead of loading a constant...)
$ cat dummy.c
int
add_random_number(int a)
{
return a + 0x12345678; /* guaranteed to be random */
}
$ arm-none-eabi-gcc -S -o- -Os dummy.c
(...)
add_random_number:
@ Function supports interworking.
@ args = 0, pretend = 0, frame = 0
@ frame_needed = 0, uses_anonymous_args = 0
@ link register save eliminated.
add r0, r0, #301989888
add r0, r0, #3424256
add r0, r0, #5696
add r0, r0, #56
bx lr ldr r0,=0x123456578
assembles to ldr r0,pc+xxx
... and then somewhere later ...
.word 0x12345678
The assembler had some default places it would put constant pools (end of a module?), or you could explicitly tell it to generate a constant pool if the default place would be outside the limit of the pc-relative addressing mode.In all my years coding in ARM assembly language, the range of immediate values it supported was rarely if ever an issue.
I haven't done much assembly in a while, but I was heavily into it once upon a time, and I recall that values with lots of 1s were useful. There is not quick way to generate those here. This means that we can write a single instruction to set any single bit using an inclusive OR and the proper immediate value, but we cannot write a single instruction to clear any single bit.
The reason I think a bit more cleverness might have helped is that there are so many values with multiple encodings. Anything where the 8-bit value ends with 0 has a different encoding as well. For example, a rotation of 0000 and an 8-bit value of 00000100 gives the same result as a rotation of 1111 and an 8-bit value of 00000001 (right?). Perhaps some of the redundant instructions could have been used to represent things ending in lots of 1s?
Regardless, an interesting and informative post. :-)
0x01000000 - 1 = 0x00ffffff
so you can generate any number of trailing ones with 2 instructions.
The specific details require you to dig a bit past explaining just the immediate encoding, but in the clearing case there's a dedicated instruction for clearing the bit specified by the immediate:
BIC - Bit Clear (immediate) performs a bitwise AND
of a register value and the complement of an immediate
value, and writes the result to the destination register.
As I mentioned in my other post, zero-rotation encodings are gamed out as well (to allow byte repetition).Ah, didn't catch that.
(well, the PDP-10 was pretty RISC for its day and gave us things like BLT, hence bitblit).
MVN r0, #0x10000000 ; ro = 0xefffffff
Oh, the joy :-).The problem as I see it with this sort of cleverness is that it's difficult to optimize to this. It leads to quite variable best-case and worst-case scenarios and general unpredictability. As, say, a C programmer and not a compiler designer, you might unintentionally pick lots of values that won't fit into the "immediate" scheme (worst-case). Or you might force your design to use numbers that DO fit into this scheme (best-case, but a bleed of lower-level design decisions affecting higher-level design decisions).
ARM really isn't that hard if you're already thinking like a low level programmer. Things like MIPS were (better) designed for being targeted from higher level languages, but the consequence is a much messier machine language. It's always struck me as amusing that the conventional view is MIPS is minimal, when ARM is really much more so, but it's from outside the Berkeley/Stanford RISC bubble so didn't really get on to their radars for some time.
Is this a more useful subset? I am guessing it is so, since they went to this trouble.
0x0000nn00
with a nonzero nn, and those are the bits that can't be gotten to from rotating.
Interestingly, there is some redundancy with this encoding meaning you can't represent a full 2^12 unique values. Eg 0x1 could be represented by 0x1 and 0x0 in the rotate field, 0x4 with 0x1 in the rotate field, 0x10 with 0x2, or 0x40 with 0x3. The same applies for every other combination - this means that there are only (2^12)/4 = 1024 unique values that are representable in total.
Doing a quick brute force test, it appears that there are 3073 unique values. I suppose that makes sense. Each new rotate introduces 192 new values that have at least one of the new bits set, and 192 * 16 = 3072. Then you have the number 0.