Hacker News new | ask | show | jobs
by adrian_b 15 days ago
When first looking at the source code, I wondered why one would waste so much time to write 25k lines in raw assembly language, but then I saw that it was generated with Claude, for whom it does not matter much how expanded is the written text.

If someone had written this program manually, the strategy would have been very different. With a good macro-assembler (and nasm is good enough) one should define a great number of macros, to encapsulate all the tedious boilerplate, especially for things like function prologues, epilogues and invocations.

With a well written macro library, an assembly program can be almost as compact as a C program, instead of containing many text lines for each equivalent high-level language statement.

Such an assembly source with good macros can be read and understood much more easily than raw assembly language, like in this "frame.asm".

Otherwise, this is interesting work.

5 comments

It’s an interesting strategy, but I question how much it pays off, if at all. Very few parts of a program benefit from manually tuned assembly compared to the naive C implementation. Writing everything in assembly adds an extra layer of thought, which even for an LLM is additional effort that could have been used for targeted optimizations instead. It makes it harder to notice patterns that have been trained into the data set, from security problems to performance opportunities.

On a long enough time frame with enough tokens invested there’s probably not a difference, but being written in assembly by an LLM doesn’t imply optimal to me. I’d almost prefer having an LLM rely on higher level abstractions offered by a programming language rather than rolling everything itself. After reviewing a lot of LLM code, even at Fable and Sol levels, I just don’t trust that LLMs are writing optimal code. Assembly makes it harder to even review.

I do it find it very fun and entertaining. This is a component and I’m grateful that it was shared.

It will make the code slower.

Writing maintainable assembly is at odds with writing fast assembly in most circumstances.

A key optimization that's hard to pull off is inlining.

An optimizing compiler can see that a method is small enough that it can be pulled into the caller, it can then further eliminate from that smaller method branches that can't be executed due to the nature of the caller (Imagine calling a function with a `bool` parameter and you send in `true` at the call site).

To make the code faster in hand written assembly, you have to do the inlining, but that makes writing more structured code a lot harder. You are duplicating logic paths in the name of performance.

Not to mention the fact that the compiler gets updated and knows about more instructions and architectures then you do or then you could have. Hard to write the FMA instruction if it didn't exist when you were writing the assembly in the first place.

Simply no. This is a catchphrase, a buzzword from compiler companies. We have seen here on HN time and time again many articles showing the truth to be the opposite of what you've said. It's time to put to rest the nonsense that compilers optimize better than humans. Here a few examples I've talked about:

https://news.ycombinator.com/item?id=8508923 https://news.ycombinator.com/item?id=36618344 https://news.ycombinator.com/item?id=41922295 https://news.ycombinator.com/item?id=44186843 https://news.ycombinator.com/item?id=44177446 https://news.ycombinator.com/item?id=36949314

Simply yes.

It's true that in specific functions a human can do better than a compiler at optimizing for a target platform (sometimes, not always). That's a place where someone could reasonably drop down to assembly and get better performance.

But the case I'm specifically pointing out, one of inlining, is something that compilers do better than humans. This isn't "propaganda from compiler companies" (btw, not a thing. The most common and popular compilers are opensource and not owned by any single company). There are other cases like this where compilers are just more likely to get things right than humans are. They have a lot of heuristics about common assembly patterns that few humans can be expected to have memorized.

Each of the cases you pointed out are cases where the compiler does a bad job at optimizing a single function for whatever reason. They are not examples of a whole application written in assembly outperforming compiled high level languages. And each of the cases almost certainly took the human a considerable amount of time to figure out and prove their solution was better than the compilers.

What you've done is cherry pick when compilers fail and you are using that as evidence that they always fail.

Compilers sometimes fail, something I'm happy to admit. But on the whole for a whole application the compilers will get a lot more right than a human possibly could because they can output unmaintainable assembly.

Claude has surprisingly good knowledge of X11 protocol.

The other day, colleague showed me a (pretty basic) terminal emulator written in one-shot by Opus. Kicker is - that was compiled to a 30 KB static binary. That's right. No libX11, no libXfont, not even libc.

Only because the X Window System is very heavily documented, e.g., in the excellent "The Definitive Guides to the X Window System Series".
Yeah, I've had it work on an X11 server using a Ruby X11 protocol implementation instead of libX11, and it just rushed ahead and added support for a bunch of missing requests and responses. None of that is hard - it's all very well documented - but it's tedious.

My terminal emulator using the same binding also started out hand-written but Claude overhauled that too recently and it knows vtxx escape codes far better than me too.

You wrote your own terminal emulator?
No libc? It used inline assembly routines?
Yes, something like this:

  static inline long read(int fd, void *buf, long count) {
    register long rax asm("rax") = __NR_read;
    register long rdi asm("rdi") = (long)fd;
    register long rsi asm("rsi") = (long)buf;
    register long rdx asm("rdx") = count;

    asm volatile(
        "syscall"
        : "+r"(rax)
        : "r"(rdi), "r"(rsi), "r"(rdx)
        : "rcx", "r11", "memory"
    );
    return rax;
  }
The linux kernel also has its own headers (IIRC it was something like <asm/unistd.h> and/or <sys/syscall.h>, but might depend on architecture and version) where it will provide the stub asm statements.

In my memory the syscall ABI has changed a few times (i386 had int $0x80, then sysenter, then abstracting it in the vdso, then amd64 has 'syscall'), so it may be easier to let the kernel header provide the mechanism.

The Linux syscall ABI is (famously) stable; on x86* you can still call into 0x80 just fine. There might be new interfaces made available, but existing programs shouldn't break. Although, in C there could be value in abstracting over the per-platform differences like int/syscall. I've only ever done raw syscalls from assembly where portability was rather moot:)
It's quite easy to get it to do syscalls directly, I even got Fable to do a simple standalone TLS implementation (in C). Not really useful since it hardcoded a set of supported algorithms etc., but it's fun to see how "simple" you can force it to go.
Once you create an abstraction (like a macro, routine, language) you forfeit the benefits using machine code gives you wherever you use that abstraction, since there could be some bespoke implementation that solves the problem.

That entire point is moot here because the abstraction in this case is a massive ball of crap and it's used everywhere. So you never get the benefit you think you would for using a lower level language. That's why generally LLMs are best with python and even better with a "harness" (domain specific framework and language)

assembler is fun and kinda easy. easier than learning any JS framework in my opinion serious here.

Modern macro assemblera are fun

Sounds like another prompt is in order to re-write the code