How x86_64 Addresses Memory
blog.yossarian.net
blog.yossarian.net
I don't think the example for this (https://gcc.godbolt.org/z/35ytYW) is correct.
The highlighted instruction ("movabs rax, offset x") is not a load, it's just moving the address of x into rax. The "offset x" operand is a 64-bit immediate (not a displacement). It will be resolved to the address of x with a relocation.
Indeed, you can get the compiler to emit this form of "movabs" into other registers, which contradicts the point that 64-bit displacements are a* specific: https://godbolt.org/z/3MYTUC
To get a load or store to a 64-bit displacement, I think you want something like this: https://godbolt.org/z/4QMtpo
I noticed a few other things in the section about segments:
> The good news is that caring about them isn’t too bad: they essentially boil down to adding the value in the segment register to the rest of the address calculation.
Surprisingly, the segment register's value isn't added directly (my coworker and I discovered this recently). The segment bases are stored in model-specific registers, and I don't believe these are readable from user-space. https://en.wikipedia.org/wiki/X86_memory_segmentation#Later_...
If you try to read %fs directly, you'll get a completely unrelated value (an offset into table I think?)
Another surprising and unfortunate thing is that segment-qualified addresses don't work with lea. That means getting the address of a thread-local unfortunately is not as simple as "lea rax, fs:[var]." You actually have to do a load to get the base address of the thread-local block (eg. fs:0). The first pointer in the thread-local block is reserved for this. That's why this function has to do a load before the lea: https://godbolt.org/z/jkt28n
And yep! I need to make the language around the segment registers more precise: if I'm remembering right, the segment value itself gives you the GDT index (maybe only in 32-bit mode?), which you can then pull from.
For 64-bit modes I think those values essentially become nonsense because of the fsbase/gsbase MSRs, as you mentioned :-)
It can be quite a good exercise to try and produce your own hex opcodes from the tables using something like CyberCheff [2].
[1] https://www.intel.com/content/dam/www/public/us/en/documents...
[2] https://gchq.github.io/CyberChef/#recipe=Disassemble_x86('32...
[1] https://www.intel.com/content/dam/www/public/us/en/documents...
register char (*(*(*(*ram)[512])[512])[512])[4096] asm(cr3)
Basically each modrm pointer thingy goes through four layers of page table indirection for each memory access, in order to turn virtual memory addresses into real memory addresses. But it's mostly an implementation detail where access to the above data structure is restricted to the operating system only, unfortunately, and the closest thing we have to using it is the mmap api.The dominant reason is: it saves registers. X86 is register starved even in 64-bit mode. Just 16 regs means shit gets spilled. If you also needed a tmp reg for each memory access, the way that makes things slower is that it causes spills - usually elsewhere - that wouldn’t have been there if the address was computed by the instruction.
It helps that CPUs make these things fast. But it can be hard to prove that using the addressing mode is faster in the absence of the register pressure scenario I described.
Even bigger deal on x86-32.
I recently wrote a x86-64 backed (as in like 4 years ago) and I recall that if address pattern matching (which is now very complete) was not there, you’d lose >1% perf across the board.
I wrote a pretty good x86-32 backend - for a totally different compiler (a whole program PTA-based AOT for JVM bytecode) a long ass time ago and vaguely remember it being like 10-15% there.
Note I’m talking about macrobenchmarks in both cases. And not individual ones - the average over many.
Also don’t forget those functions that sit on the ledge of caller saves. Having any caller saves is more costly than having none. So if a function is using all of the volatile regs, makes no calls (so no need to promote to caller save), and you cause it to use one more register, then it’s prologue/epilogue slows down.
Registers matter a lot. :-)
And about your point about freeing up ports or other things: that may be a cool theory, but I’m just saying that it’s hard to show that using those addressing modes is a speedup if register allocation doesn’t care either way (I.e the use of addressing modes doesn’t help spills or prologues). Meaning, most of the reason why compilers emit these is for regalloc, and it is the only reason that I’ve been able to detect as being the one that changes perf across two different back ends.
Maybe you personally weren't able to prove to yourself if you did some very small microbenchmarking, but I really believe that measuring the differences of bigger code built with one or another approach it should be relatively straightforward to demonstrate the advantage of using the more compact instructions.
These were giant macrobenchmarks. And I said hard, not impossible. As in, most code doesn’t care if you shift or lea, unless it affects regalloc, which it almost always does.
It’s true that having smaller code is better regardless of perf - so if perf was neutral we would still use those instructions.
The benefit of the smaller instructions for perf is better register allocation, as I said. So, for macrobenchmarks, using the smaller instructions is a win every single time. And that win comes mostly from fewer spills. That’s the point of what I’m saying.
I suppose someone could do the experiment of turning on instruction selection patterns for address modes but still “pinning down” a register as if it was needed for the shift-add sequence you would have otherwise emitted. Feel free to do that if you want to prove me wrong. But just hand waving that I must not have run big benchmarks isn’t going to help you learn more about compilers.
Starting with that as a thought experiment, isn't it expected that the bigger code can be shown to execute slower, as soon as the performance isn't measured with a microbenchmarking test which avoids to stress caches?
Or to be more specific, imagine starting with your proposed modification of the compiler and recompiling both an OS and all the applications. Would the resulting change in the performance be measurable or not? I honestly can't imagine how it wouldn't.
I agree that it “should” be so. I’m just saying that based on data I’ve seen so far, I don’t think it actually is. But only one way to find out, and that’s to run the experiment.
Note that some apps care about icache for perf and some kinda don’t. Sometimes the thing that the program is blocked on in the cpu is data or something else, not instruction fetching, so if you make the fetching slower it won’t affect the end-to-end perf. Basically run time is not a linear function of overheads but rather something much more complex since there is queueing and asynchrony going on. It’s possible for one element of a program to be worse but it doesn’t affect running time because the CPU is blocked elsewhere.
And lets stop talking about microbenchmarks, ok? I don’t use those. None of my claims are based on them.
> Or to be more specific, imagine starting with your proposed modification of the compiler and recompiling both an OS and all the applications. Would the resulting change in the performance be measurable or not? I honestly can't imagine how it wouldn't.
Yeah, that’s an experiment someone could try.
I think probabilistically so I don’t want to say that I believe that the experiment will definitely go one way or another. But I can give you my odds:
- 20% chance you see a speedup of any kind, and only 5% chance it’ll be uncontroversial (remember when you test big shit, somethings will be faster and others slower and you’ll have noise - so good chance you’ll see data that makes you feel like something is just wrong). - 80% chance that speedup is less than a quarter of the speedup if you hadn’t pinned down the register.
On x86, I think that an instruction with a memory address always takes a cycle to compute it no matter how simple or hard it is, so if you do the computation in a separate instruction, you are likely to add cycles.
The one that I remember puzzling with back in the day was 'SEI' - I mean, why have an interrupt disable bit? Wouldn't it be more sensible to set and clear interrupts, not set and clear disabling interrupts.
Am I the only one seeing this?
Thus, I spend quite a bit of thought, trying to infer the historical constraints and motivations that give us x86's beauty, but I'd love to have some resources that could flesh out my understanding in this regard.
Paging is an old OS concept and predates Intel CPUs and even Unix. How far back in time do you want to go?
I think some concepts which were influential in the early days of Linux are well covered in Minix (1.0) code and book (after all, Linus first experimented with a 386 scheduler for Minix).
Would it fair to say that the following can be considered an encoding of an instruction? ADD rax, rax
Since if we do then we can actually say x86 has 2 forms of encoding. It's assembly form and it's binary form which is abit interesting.
I don't know why people still use these crazy names. x86_64, x64, etc. The people who designed it call it AMD64. Let's call it that.
The K6 was a very competent chip but the you can see in the how the high level architecture starts to look much more like the EV6 with the Athlon series.
https://software.intel.com/content/www/us/en/develop/article...
> Near branches with the 0×66 (operand size) prefix behave differently. One type of CPU clears only the top 32 bits, while the other type clears the top 48 bits.
This is more than enough to send your code into wrong address and possibly cause a crash.
FWICT, "x64" is mostly limited to Microsoft. I wouldn't mind that one being thrown out.
Yes, that one particularly, since 86 and 64 don't even have anything to do with each other. One is a product number and the other is a word width - why did they replace one with the other?!
"x86" fits nicely in there and they couldn't refactor it in time, so they just decided to go with "x64".
For example the windows build flavor is called amd64fre or amd64chk. (chk is a debug build, fre is what most humans use)
They did not, at the time. I have a set of original manuals for the AMD "x86-64" architecture (with a dash, not an underscore).
Why does everyone want to invent their own name for this thing?
There are contexts where hyphens are not allowed but underscores are, like identifiers in many programming languages. Replacing the hyphen with an underscore is an obvious workaround.
Because one is a legal part of a symbol name in C and the other only works in lisp.
They originally called it "x86-64". Recycling a comment of mine from 4 months ago (https://news.ycombinator.com/item?id=22282127):
The "x86-64" name is the original one, and came from AMD themselves: https://web.archive.org/web/20000817071303/http://www.amd.co... (and "x86_64" is obviously an alias for where a hyphen is not an allowed character, like identifiers on many programming languages).
The "x64" name came from Microsoft, probably due to file name length limitations (this was before Windows XP unified the Windows 9x and Windows NT lines).
IIRC, the "AMD64" name came later, probably to distinguish it better from Intel's IA-64 (Itanium).
If we’re speculating, my guess is that they chose “x64” for symmetry with “x32”. Or woodruffw’s suggestion: https://news.ycombinator.com/item?id=23513591
x32 only came out around 2011. It's younger than x64 which came out around 2003, so that can't possibly be the case.
Looks like “x32” was used at least as far back as 2009: http://www.utteraccess.com/forum/office-2010-x64-bit-qu-t191... . Not much earlier than 2011, but just FYI.
It was announced in August 2011, that's when the code was shown for the first time.
https://lkml.org/lkml/2011/8/26/415
> Looks like “x32” was used at least as far back as 2009: http://www.utteraccess.com/forum/office-2010-x64-bit-qu-t191.... .
This is a great example because that person is confused by these terrible names - they've seen the name x64, and they assume there must be an x32... but there wasn't, back in 2009. They mean 32-bit x86, which is not the same thing as x32.
Anyway, the tone of the article is unnecessary, IMHO. These addressing modes are useful and easy to understand, and the address generation units do double-duty as low-latency, high-throughput add-and-shift units, via the LEA instruction. CISC is useful, after all.
Otherwise smart people miss simple things all the time.
For example,
lea eax, [ecx+ecx*8+42]
is equivalent to eax = ecx * 9 + 42, so you get a multiplication, an addition, and a move (without destroying the source register) in one instruction.or the FLAGS register. Which can be very useful in some spectacular corner cases.
With respect to the tone: it's a little flippant, sure. I work professionally on research programs that involve binary translating x86 (and other CISCs) into various representations for program analysis; what you're seeing is some of my frustration there bubble up.
While I think you are right that LEA exists because of the memory addressing modes, at least on Intel (and I'm pretty sure AMD) it's been a long time since it's actually been executed on the address generation units. Instead, it's [mostly] treated as just another arithmetic instruction and executed on the same integer ALU's that execute all the other simple integer math. According to Agner (https://www.agner.org/optimize/instruction_tables.pdf) it's been this way at least since the original Pentium.
Does anyone know what the last mainstream processor was that actually executed LEA on the AGU? And whether there any less mainstream processors that still do?
[mostly] I say mostly because there are a few odd addressing modes that have longer than usual latencies, and a because the 3 argument form also takes longer than the usual 1 cycle latency. It's still executed on a standard integer port, though, and not on the AGU.
https://www.mikeash.com/pyblog/friday-qa-2012-07-27-lets-bui...
And for ARM.
https://www.mikeash.com/pyblog/friday-qa-2013-09-27-arm64-an...
Except the designers foresaw this and established Canonical Addresses[0] to prevent people from using that "unused" space for tags. The space is explicitly reserved. This is probably why LuaJIT uses NaN tagging of doubles instead of tagged pointers.. even though that causes an issue of it's own[1].
[0]: https://en.wikipedia.org/wiki/X86-64#Virtual_address_space_d...