HNHacker News
TopNewBestAskShowJobs

mbitsnbites

383 karma · joined March 21, 2018

submissionscomments
mbitsnbites··on Quake on an FPGA (MRISC32 CPU) [video]
> Keep in mind that the average RISC ISA uses 5 bit registers IDs and uses three-arg form for most instructions, that's 15 bits gone. While AMD64 uses 4 bit register IDs and uses two-arg form for most instructions, which is only 8 bits.

Yes, I'm aware of that (that's what I meant with "destructive operands").

But then you have AVX-512 that has 32 architectural registers, and some instructions support three register operands (LEA, VEX-encoding, ...). So there you have the choice of having multiple encodings that will be more compact, or a few simple encodings that will require wider encoding.

Since x86 isn't known for having a small and consistent instruction set, any attempts to streamline the post-decode encoding will likely gravitate towards a wider encoding (i.e. worst case least common denominator).

It would be an interesting exercise to try to fit all the existing x86 instructions (post-cracking) into an encoding of 32, 48 or 64 bits, for instance. But I don't envy the Intel & AMD engineers.

mbitsnbites··on ZX Spectrum Raytracer
> The ZX Spectrum has a 3.5 MHz Z80 processor (1,000 times slower than current computers)

Actually, it's much, much slower than that.

The Z80 takes at least one clock cycle to add or subtract an 8-bit number (sometimes several clock cycles, depending on addressing mode). For a 32-bit number those clock cycles add up quickly.

In contrast, a modern 3.5GHz CPU can add or subtract several 64-bit numbers every clock cycle

And don't get me started on multiplication or floating-point arithmetic. A modern CPU would be thousands times faster, even if it was running at the same clock speed.

Thus, I think that the feat is even more impressive than presented!

mbitsnbites··on Quake on an FPGA (MRISC32 CPU) [video]
BTW... Except for the indications from the die shots, one of the reasons that I don't think that uOPs can be as small as 32 bits is that studying fixed width ISAs and designing MRISC32 have made me appreciate the clever encoding tricks that go into fitting all instructions into 32 bits.

Many of the encoding tricks require compiler heuristics, and you don't want to do that in hardware. E.g. consider the AArch64 encoding of immediate values for bitwise operations.

Also, even if you manage to do efficient instruction encoding in hardware, you will probably end up in a situation where you need to add an advanced decoder after the uOP cache, which does not make much sense.

The main thing that x86 has going for it in this regard is that most instructions use destructive operands, which probably saves a bunch of bits in the uOP encoding space. But still, it would make much more sense to use more than 32 bits per uOP.

mbitsnbites··on Quake on an FPGA (MRISC32 CPU) [video]
> I found annotated die shots of Zen 3 and Zen 4

Ooo thanks! Sure looks like strong evidence.

> TBH, we have no idea how big the x86 tax is.

No, and it gets even more uncertain when you consider different design targets. E.g. a 1000W Threadripper targets a completely different segment than a 10W ARM Cortex.Would an ARM chip designed to run at 1000W beat the Threadripper? Who knows?

> Intel and AMD simply aren't incentivised to optimise their designs for low power consumption as their cores simply aren't used in mobile phones where ultra low power consumption is absolutely critical.

They'll keep doing their thing until they can't compete. They lost mobile and embedded, and competitors are eating into laptops and servers where x86 continues to have a stronghold. But perf/watt matters in all segments these days, and binary compatibility is dropping in importance (e.g. compared to 20-40 years ago), much thanks to open source.

IMO the writing is on the wall, but it will take time (especially for the very slow server market).

mbitsnbites··on Quake on an FPGA (MRISC32 CPU) [video]
Good points. I guess it's a debate of nomenclature, which lacks real importance (although it helps to reduce confusion).

My point of view is mostly that, no, the x86 architecture certainly is not load-store, but internally modern x86 machines have execution pipelines that are built like regular load-store pipelines (i.e. the topology and flow is mostly the same as a RISC-style load-store pipeline).

Or to put it another way, x86 execution pipelines are much closer to being register-register than being register-memory.

> No, they designed and evolved the pipeline and its internal ISA to directly match the x86 ISA they needed to run.

Yes. That is very true. Although the front end is the part of the pipeline that is most x86-specific, there are many parts of the rest of the pipeline that is tailored to be optimal for x86 code. It was obviously not designed in a vacuum.

An interesting observation is that even other ISA:s and microarchitectures have been influenced by x86 (e.g. by including similar flags registers in the architectural state), in order to not suck at emulation of x86 code.

mbitsnbites··on Quake on an FPGA (MRISC32 CPU) [video]
> According to the label, that block contains both the uop cache AND the microcode ROM

Yes, so it's hard to tell the exact size. We can only conclude that the uOP cache and the microcode ROM combined are about twice the size of the L1I cache (in terms of memory cells).

Another core die shot of the Zen 2 micro architecture is this (it appears to be correct as it is based on official AMD slides): https://forums.anandtech.com/proxy.php?image=https%3A%2F%2Fa...

Here uCode is in a separate area, and if we assume that the SRAM blocks in the area marked "Decode" represent the uOP cache, then we have:

* The uOP cache has the same physical size as the L1I cache

* uOP cache size = 4K uOPs

* L1I cache size = 32 KiB ~= 8K x86 instructions

If all this holds true (it's a big "if"), the number of uOP instructions that the uOP cache can hold is only half of the number of x86 instructions that the L1I cache can hold, and the size of uOP entries are in fact close to 32KiB / 4K uOPs = 64 bits each (given how similar the SRAM cells for the two caches are on the die shot I assume that they have the same density).

Furthermore I assume that one x86 instruction translates to more than one uOP instruction on average (e.g. instructions involving memory operands are cracked, and instructions with large immediates occupy more than one uOP slot - even the ARMv8 Vulcan microarchitecture sees a ~15% increase in instructions when cracking ARM instructions into uOPs: https://en.wikichip.org/wiki/cavium/microarchitectures/vulca... ), which would mean that the silicon area efficiency of the uOP cache compared to a regular L1I cache is even less than 50%.

Edit:

> Nothing is free in CPU design, it's just a massive balancing act.

Yup, and a large part of the x86 balancing act is to keep the x86 ISA alive and profit from the massive x86 ecosystem. Therefore Intel and AMD are prepared to sacrifice certain aspects, like power efficiency (and presumably performance too), and blow lots of energy on the x86 translation front end. That is a balancing act that designers of CPU:s with more modern ISA:s don't even have to consider.

mbitsnbites··on Quake on an FPGA (MRISC32 CPU) [video]
Thanks, I have read the Agner documents before. I will dig around some more and get updated.

Anyway, I found this, regarding RMW (for Ice/Tiger Lake):

> Most instructions with a memory operand are split into multiple μops at the allocation stage. Read-modify instructions, such as add eax,[rbx], are split into two μops, one for address calculation and memory read, and one for the addition. Read-modify-write instructions, such as add [rbx],eax, are split into four μops.

I read it as a instructions that use memory operands (other than simple mov instructions) are usually split into at least two uOPs, which makes perfect sense for a load/store pipeline.

> So it's far from a proper load/store pipeline. And when you think about it, that makes sense, x86 isn't a load/store ISA so it would be wasteful to not have special accommodations for it.

The way I see it, modern x86 microarchitectures are load/store. My definition of load/store is that all instructions/operations that flow through the execution part of the pipeline can either load/store data OR perform operations on registers, not both (except possibly edge cases like calculating an address or writing back an updated address to a register).

That is by far the most efficient way to implement a CPU pipeline: You don't want to read data in one pipeline stage, use the data in an ALU in a later stage, and possibly write data in an even later stage. That would drastically increase instruction latency and/or require duplication of resources.

This is, AFAIK, one of the main advantages and probably the raison d'être for uOPs is the first place: translate x86 instructions into uOPs (multiple ones for instructions that access memory) so that the pipeline can be implemented as a traditional load/store pipeline.

In a way the x86 front end is similar to software binary translation (a'la Transmeta, NVIDIA Denver or Apple Rosetta 2). It's fairly complex, and the prime objective is to take code for a legacy ISA and transform it into something that can run in a pipeline that the ISA was originally not intended to run in. By doing the translation in hardware you avoid the latencies inherent to software translation (JIT or AOT), but the costs are unavoidable (particularly silicon area and power consumption).

mbitsnbites··on Quake on an FPGA (MRISC32 CPU) [video]
I'll have to read up on Agner's findings.

My assumptions are largely based on annotated die shots, like this one of Rocket Lake (IIRC): https://substackcdn.com/image/fetch/f_auto,q_auto:good,fl_pr...

If they are correct, the uOP cache consumes at least as much silicon as the L1I cache, while they generally can hold fewer instructions.

Some napkin math: x86 instructions are 4 bytes long on average, so a 32KiB L1I can hold 32/4=8K instructions, while the uOP cache can hold 4K uOP instructions (how many uOPs does an x86 instruction translate to on average?). That would indicate that uOP:s require twice the silicon area to store compared to "raw" x86 instructions - or that the uOP cache is more advanced/complex than the L1I cache (which may very well be the case).

Also visible from the die shots: decoding and branch prediction are far from free.

mbitsnbites··on Quake on an FPGA (MRISC32 CPU) [video]
> The average A64 instruction does slightly less work than the average AMD64 instruction so the net result is that it makes sense to have slightly wider decoders

I'm not sure that the difference is that big. A64 actually has quite powerful instructions, and some of them do more work than similar x86 instructions (madd and ubfx come to mind). In my testing A64 code often has fewer instructions than x86: https://www.bitsnbites.eu/cisc-vs-risc-code-density/

> X86 CPUs don't quite use a "RISC-like encoding". The µops support RMW for memory, for example.

I would love to learn more about that. Do you have any references? I was under the impression that internal instructions followed the load/store principle since I assume that the internal pipeline is a load/store pipeline?

> The Power CPUs call it "cracking" when complicated instructions are split into simpler µops.

Yes, it's the IBM term AFAIK. They call it cracking in zArch too. I also suapect that at least some ARMv8/9 implementations do cracking too (many AArch64 instructions have multiple results, which might be better handled as multiple internal instructions - I think it's partly a code density thing).

mbitsnbites··on Quake on an FPGA (MRISC32 CPU) [video]
> Single-length instructions, for example. Even x86 instructions aren't much of a problem when you can afford to throw more pipeline stages at the problem

I think that the problem is bigger than that. Sure, the branch predictor usually keeps even the x86 pipeline busy, but x86 variable length instruction encoding becomes a problem for at least three problems:

1. Decoding width - There is a practical limit to how many instructions you can decode in parallell. You can add pipeline steps, but at some point it becomes absurd.

2. You get a very big range of minimum-to-maximum number of bytes that you need to fetch to extract a fixed number of instructions each clock. E.g. an eight-wide front-end would have to fetch 8-120 bytes per clock cycle. And it gets worse since your next eight instructions may not start on a nice power-of-two boundary, so you have to fetch much more, e.g. 256 bytes per cycle, in order to cover the worst case scenario (compared 32 bytes per cycle for a fixed width 32-bit RISC encoding). And you may gate cache line / page crossings in your "bundle".

3. Since you need to do fairly heavy translation work into an internal RISC-like encoding (which, by the way can not be as compact as compiler-generated RISC instructions - you typically need 64 bits per internal instruction or similar), you need to cache your translations into a uOP cache (or L0 cache). This cache uses much more silicon per effective instruction than a regular L1I cache, so it can not hold as many instructions (and I'm pretty sure that most of the instructions in the L0 cache are stored in the L1 cache too - so not really extra cache memory). All this silicon could be used for a larger L1I cache, for instance (or a better branch predictor).

So, yes, branch prediction really helps, but it does not solve all problems.

mbitsnbites··on MRISC32 – An Open 32-Bit RISC/Vector ISA (Suitable for FPGA CPU)
> 1) you could argue that's bad code generation, which could easily be fixed in the compiler, as `a1` could be used as the result of the `add`

Yes, as I said.

Please note that the code was generated by GCC (trunk) that has fairly mature RISC-V support (it has had RISC-V support since 2017 at least, and the fusion optimization was suggested even earlier than that). If fusion of load/store instructions is indeed an important part of the RISC-V design, why has this not been fixed in the compiler during the last 6+ years? I take it as there is no hard canonical way that register allocation should be done in these situations, which means that HW implementations must assume that such a sequence may clobber two registers.

> 2) since late 2021 RISC-V has the `sh2add` instruction

Which is great. As you say, not really a win in terms of code density, but definitely fewer degrees of freedom and only one clobbered register. Still not as good as dedicated indexed load/store instructions, though.

BTW, while I agree that an indexed load without indexed store would be better than nothing (I actually considered that for MRISC32), I think that it would pose a problem for compilers that generally assume symmetry (addressing modes supported by loads are assumed to be supported by stores too).

> 3) using a function like this to prove anything actually verges on dishonest (and erincandescent did the same thing) because no real program where anyone cares about performance will have such a function in hot code

True that GCC happily and often transforms indexing inside loops to address increments rather than index increments (even so for architectures that has support for indexed addressing).

The point of the example, though, was to highlight that when the compiler needs to do indexed addressing, it must clobber unrelated registers, which is a problem for instruction fusion (i.e. a fused instruction does not do the same thing as a corresponding architectural instruction, as the fused instruction has more side effects).

> It gives optimal price-performance.

Probably. But this circles back to the question about what segments you target with your design. I am fully convinced that RISC-V is the best ISA choice for many segments, especially the cost sensitive segments. However I'm less convinced that RISC-V (as it is) is the optimal choice for less cost sensitive high performance segments (e.g. premium products like those from Apple, gaming riggs, compute servers, and so on). It can probably do a good job there (just like x86 can do a good job, despite all its ISA bloat and shortcomings), but if the integer ISA was designed with support for three source operands from the start, it would be an even better fit for those segments (at least that is my belief).

mbitsnbites··on How to Design an ISA
I think that companies will continue to suggest a "big bang" that involves supporting integer instructions with three source operands and possibly dropping compressed instructions.

Maybe it's out of selfishness (e.g. because they are repurposing an existing microarchitecture for RISC-V), or maybe it's because that's how they want to build their hardware (e.g. for their particular performance target decoding a plain-old 32-bit instruction may be more silicon/power efficient than to fuse 2-3 16-bit instructions).

Whatever the reasons, I think that RISC-V will have to live with this critique for as long as it lives.

I'm not saying that those are poor design choices, but they will always be pain points (for small cores and big cores alike - but for different reasons).

I don't think that you should underestimate the drive to modify an architecture if it does not fit your needs - especially if it is a free and open architecture like RISC-V. For example LoongArch has already happened, and I can easily see how something similar can happen if a major player decides to move from x86 or ARM to "something else" (e.g. if NVIDIA wants full control over their next gen super AI solution).

mbitsnbites··on How to Design an ISA
Yes, but that's a big "if". When you start looking at it from the perspective "I don't need binary compatibility with microcontrollers", you realize that opcode space has been wasted on things that you don't need. It's similar to how x86 has wasted several single-byte encodings on instructions that are never used in modern programs. The extensions that you end up wanting to do overlap with the base ISA, but with slightly different semantics.

It's manageable and you can live with it, but already from the start you have an unnecessary legacy that needs to be handled.

If there is enough consensus in the industry, a new revision may be the best way forward.

mbitsnbites··on How to Design an ISA
I would say that all modern "general purpose" ISAs (and microarchitectures) are optimized for C (and C++). That's what most OS:es and high profile applications are written in (web browsers - and by extension electron apps, compilers, games, content creation, etc).
mbitsnbites··on How to Design an ISA
In that case you don't need a canonical order. The instructions do not even have to be next to each other.
mbitsnbites··on Quake on an FPGA (MRISC32 CPU) [video]
Thanks a bunch for the link! I love how you fit a quake renderer onto such a small device (having a demo scene size coding history, I enjoy the challenge of a constrained target and I'd like to do something similar with the MRISC32 ISA some time - e.g. an "MRISC32-A0").
mbitsnbites··on How to Design an ISA
I think you're onto something, but I'm not entirely sure that that's exactly how it's going to play out.

First of all I don't think that "compatibility will reign". It's more like once the industry really starts picking up RISC-V, fragmentation will reign (at least for a decade or so).

It's also quite likely (IMO) that we'll see a "next generation" rather sooner than later, i.e. "RISC-VI". RISC-V, with some agreed upon extensions, may become the norm for Android, mobile, automotive and so on, but for the high end (servers, gaming, etc) I think that the industry will push for a different philosophy than the RISC-V authors originally envisioned - and that could become a new "revision" if you will.

mbitsnbites··on How to Design an ISA
Vircon32 looks very nice!

So what did you do w.r.t. addressing modes? Do you have memory-memory operations? Is it a load/store architecture?

I found the CPU specification (https://github.com/vircon32/Vircon32Documents/blob/main/Spec...), but I couldn't find an ISA specification.

mbitsnbites··on How to Design an ISA
This was a very good read! Many spot-on observations.

I actually started the design of the MRISC32 ISA before I knew about RISC-V (I even called it VRISC first, for "Vector-RISC", but had to give that name up for obvious reasons).

Initially I attacked the problem from a software developer perspective, trying to keep an open mind towards various possible solutions, but over the years I have learned lots of things and have scrapped many ideas that simply would not work well in hardware. Developing an FPGA implementation in parallel with developing the ISA certainly helped alot.

One of the luxuries of running it as a one-man open source project is that you are not bound by business deadlines and goals, so it's perfectly OK to change your mind half-way in, and let ideas and concepts mature as you learn more. And unlike the very large body that is RISC-V, you don't have to struggle with competing interests either.

mbitsnbites··on Quake on an FPGA (MRISC32 CPU) [video]
I have dodged most problems with speculation by executing the branch early, before the next instruction can enter the execute stage.

That's one of the reasons why I have very simple branch conditions. E.g. I don't have the MIPS / RISC-V "branch if two registers are equal/not equal/...", only "branch if register is zero/negative/positive/..." - which saves gate delay (if ever so slightly), and makes it possible to invalidate all the following instructions in the pipeline without needing a branch delay slot or advanced speculation logic for instructions that have already entered the EX stages.

In the diagram, as much work as possible is done in "Branch 1" (before register content is available), e.g. calculate the branch target address. All that is done in "Branch 2" is to compare the register value to fixed values (zero and -1) and check the most significant bit (sign bit), and based on the branch condition (from the opcode) determine if the branch was taken or not.

(There is some extra trickery going on in order to handle branch prediction - mostly in parallel to evaluating the branch condition)

Thus, the branch misprediction penalty is three clock cycles.

mbitsnbites··on MRISC32 – An Open 32-Bit RISC/Vector ISA (Suitable for FPGA CPU)
True, but for loads there is at least a theoretical way to avoid clobbers. For stores there is no way.

Stores: 1-2 superfluous results

Loads: 0-2 superfluous results

mbitsnbites··on MRISC32 – An Open 32-Bit RISC/Vector ISA (Suitable for FPGA CPU)
It's a shortcoming in MRISC32 too.

Unlike RISC-V, MRISC32 has a madd-instruction (integer multiply+add), which is helpful in multi-precision multiplication, but there is little help in terms of carry addition arithmetic. Together with the very verbose function prologue/epilogue (explicit stores and loads from the stack, just like RISC-V, except without the "compression trick"), this is one of the main pain points of the ISA (although I don't have extensive experience with where this really matters - except obviously for 64-bit arithmetic on a 32-bit architecture, which will be less of a problem in MRISC64).

I have a couple of ideas that I intend to explore in the future. One idea that I like, but that would be a slight paradigm shift for the ISA, is the "instruction decorators" that Mitch Alsup has developed for his My 66000 ISA: You can prefix a series of instructions with a "CARRY" meta-instruction, that basically tells the CPU that "for the N following instructions, propagate a carry via register Rx" (or something similar), effectively turning 2-in-1-out instructions into 3-in-2-out instructions. It's more of an encoding trick, and works with more instructions than just "ADD".

The tricky parts (i.e. that modifies the architecture) would be 1) instructions can have two results, and 2) the decoration instruction adds state to the decoder that must be carried over during exceptions and similar. The latter may be possible to dodge, though, if certain restrictions are put in place (e.g. no exceptions, branches, speculation or anything similar can happen during a "decorated bunch").

mbitsnbites··on Quake on an FPGA (MRISC32 CPU) [video]
Right now: assembly only. I don't really know compiler internals so it's an uphill battle.
mbitsnbites··on Quake on an FPGA (MRISC32 CPU) [video]
Checks out
mbitsnbites··on MRISC32 – An Open 32-Bit RISC/Vector ISA (Suitable for FPGA CPU)
For loads you can usually hide the clobbering by using the intended target register (the one you want to load to) as the temporary register, so e.g:

      // a0 = load [a1 + a2*4]
      slli    a0,a2,2
      add     a0,a0,a1
      lw      a0,0(a0)
...but not always.
mbitsnbites··on MRISC32 – An Open 32-Bit RISC/Vector ISA (Suitable for FPGA CPU)
An MMU is somewhere on the TODO list. I'm still focusing on user space and I still have not added support for interrupts or exceptions.

Exceptions in particular is something that I want to research more before I dive into implementation. So far I am of the opinion that memory faults is the only exception kind that you actually expect to see during normal program execution (IEEE-754 exceptions don't count, they are archaic and not really necessary). All other exceptions are exceptional and usually result in abnormal program termination (i.e. there's really no need to provide low latency precise exception handling for them).

With that in mind, I want to investigate how to best handle memory faults, rather than just implement exceptions (although memory faults are usually handled by exceptions).

There's also the issue that I want to support gather-scatter vector memory operations, which can be tricky w.r.t. memory faults (potentially a single instruction can produce one memory fault for every vector element).

mbitsnbites··on Quake on an FPGA (MRISC32 CPU) [video]
So the demo is actually running on a DE0-CV, with a Cyclone V.

A key thing for running Quake at acceptable speeds is to have a proper FPU, and most hobby softcores tend to exclude those.

I actually expect the CPU to run even faster (same clock, but fewer stalls), once I get around to improving a few things. I am seeing roughly 2 CPI in Quake right now, and I hope that there is potential to get closer to 1 CPI.

BTW, I fake-it-til-I-make-it with timing. I usually blow the timing budget when I go above 70 MHz, but I just run it 100+ MHz anyway (i.e. I'm effectively "overclocking"). As you may tell I'm a beginner myself (this is my first proper FPGA project, except for a master thesis project a couple of decades ago).

mbitsnbites··on Quake on an FPGA (MRISC32 CPU) [video]
110 MHz
mbitsnbites··on MRISC32 – An Open 32-Bit RISC/Vector ISA (Suitable for FPGA CPU)
Hi Bruce! :-) Well, to be fair I said "inspired by", not "copied from".

BTW, I'm not trying to bash RISC-V (although it may come across that way sometimes).

mbitsnbites··on MRISC32 – An Open 32-Bit RISC/Vector ISA (Suitable for FPGA CPU)
> the addressing modes I like having aren't there, so I made an ISA with them

Not really... Se my other comment: https://news.ycombinator.com/item?id=39042048

I'm not alone. Many others with far more experience and know-how in both ISA design and micro architecture design have the same opinions.

It's not that the RISC-V solution is "wrong" or "bad", it's just that it was designed to cater to all needs, including the super-simple very low end (the kind that you can implement from scratch in a weekend). I think that RISC-V does that very well, but it comes at a cost. Being good at everything pretty much precludes being the best in every segment.

MRISC32 tries to be one step up from RISC-V in terms of performance targets (well, realistically MRISC64 would be that ISA), but one step down from AArch64 in terms of complexity.

← PreviousPage 2 of 4Next →