New inline assembly syntax available in Rust nightly
blog.rust-lang.org
blog.rust-lang.org
https://github.com/Amanieu/rfcs/blob/inline-asm/text/0000-in...
Last year I had some fun getting a rust program to run on some old 386 DOS SBC I had hanging around. Was terrible in terms of code density (basically a bunch of 32 bit instructions prefixed with the "execute 32 bit op in real mode" prefix).
This was a wrapper around the DOS string print function (where s is a &str).
rusty_asm! {
let ptr: in("{dx}") = s.as_ptr();
let len: in("{cx}") = s.len();
clobber("ah");
clobber("al");
clobber("bx");
asm("volatile", "intel") {
"mov ah, 0x40
xor bx, bx
int 21h"
}
} asm!("
mov ah, 0x40
xor bx, bx
int 21h
",
in("dx") s.as_ptr(),
in("cx") s.len(),
out("ax") _,
out("bx") _,
)
However, I'd personally write that like this instead: asm!(
"int 21h",
inout("ax") 0x4000 => _,
inout("bx") 0 => _,
inout("cx") s.len() => _,
inout("dx") s.as_ptr() => _,
)
That lets the compiler handle filling in ax and bx, and also avoids trusting that the interrupt routine leaves ax/bx/cx/dx untouched. (I might also write clobbers for every other general-purpose register, too. Take a look at Linux arch/x86/boot/bioscall.S and the "Glove box" for BIOS calls, due to misbehaving BIOSes that clobber registers they shouldn't.)It would be delightful if GCC were to adopt something similar after this stabilizes in Rust.
Modula-2, Pascal, C, C++, Basic based compilers with inline Assembly parsers or intrisics that interact with host language type system instead of manually dealing with strings UNIX style.
The asm macro does know about what group of registers (or what specific register) a input/output is stored/loaded from and compilation will fail if the input isn't compatible with the register.
Then it's delegating to a assembler for the rest.
Sure it could also have a list of all asm commands the given target does support and parameters/registers it can be used with but this means keeping track of all supported targets and all commands for all potential featuresets of all targets and all way they can be called. This is a lot of work. Even more such support can be added later one, too. For now it's more important to support asm on stable.
Also with proc-macros you can implement this as a library on top of `asm`!
Also with a more "knowledgeable" asm any support for a new platform would need to manually add support for all asm of that language so having a standard stringly interface is a good think anyway.
Saying that fits with the type system is a stretch... it is like any other FFI.
The important part, type system integration, is already there. Sizes must match, move and borrow checking still happens, etc. This avoids the typical failure modes of unix-like stringly-typed systems. In fact if you read the announcement and RFC, you'll see that this was a primary goal of the new design!
I think they were absolutely right not to go down that route, because then you have to basically specify this syntax for every possible architecture targeted by Rust. Integrating ASM in the language syntax is fine if you only care about x86, but it's going to be a mess if you want to support MIPS, ARM32, AARCH64, Sparc and whatnot because the ASM dialects for each of these architectures have special bespoke syntax to deal with various quirks of the underlying ISA. MIPS has special syntax to load the top and bottom half of an address as well as dealing with delay slots (.noreorder), ARM has special sugar for PC-relative addressing etc...
Importing that baggage into Rust would be a fool's errand IMO. It would also make it harder both to port Rust to new architectures and to transfer existing assembly code into Rust since it would require adjusting the syntax.
I'm very happy with Rust's current approach, it's a good middle ground IMO.
function MultiplyBy2Reg(aInput: int64): int64 assembler
register;
asm
SHL RAX, 1;
end;
function MultiplyBy2(aInput: int64): int64 assembler;
asm
mov RAX, aInput;
SHL RAX, 1;
end;This is a very nice feature to have if you know your environment is overwhelmingly tied to a certain architecture and you want to make it easy to target this architecture in particular.
This might actually be a somewhat reasonable, if short-sighted approach if we were in 2005 and you could make the point that anything that's not x86 is effectively legacy or niche but nowadays you need to at the very least support x86-64, ARM32 and AARCH64. Baking all these flavors of assembly into a language would be very heavy handed and a maintenance nightmare.
Beyond that we're not in the 90's anymore, ASM is not routinely used outside of very low level code these days. Even for performance people often opt for intrinsics instead. Compilers got massively better at optimizing code while humans got worse due to ever more complex architectures.
Being able to seamlessly blend ASM with normal code isn't that much of a killer feature anymore IMO. It would be a costly gimmick to implement.
Also that same compiler still does it for x86.
Most Rust code is quite portable, targeting ARM, x86, MIPS, PPC, WASM, Sparc, s390x, riscv, ... That means, that for many snippets of inline assembly, you might encounter ~8 of them, one for each architecture, all using different syntaxes.
Intel syntax is quite similar to that of other popular architectures `op dst, args...`.
Adding another second syntax for x86 just doesn't add that much value IMO, and adds quite a bit of cost: now everybody dealing with x86 assembly needs to learn 2 syntaxes... and everybody dealing with portable code now needs to be at least able to read 2 syntaxes for x86... Without talking about the cost of implementing a second syntax in the compiler, etc.
If you prefer AT&T, you can always write a proc macro that translates it to Intel, and use that in your projects.
If I ever need to deal with such code, I'd just expand the macro to read the actual Intel syntax, modify that, and either fork the project, or submit a patch with a fix using Intel syntax.
If you prefer AT&T (or you have a large body of existing AT&T code you don't want to have to translate all at once), use asm!("...", options(att_syntax)) and it'll Just Work.
Nowadays GCC and LLVM support both styles and archs pick whatever they prefer and nobody cares, really.
> either fork the project, or submit a patch with a fix using Intel syntax.
That sounds a bit extreme? Reading/writing in both styles is not an issue for anyone that has dealt with x86 professionally.
Is this actually true? Admittedly I've done mostly x86 and ARM for the past several years (almost entirely Cortex-M, so v6-M and v7-M profiles, using ARM's GCC builds for embedded) and the only toolchain that prefers AT&T syntax is x86 GCC and those explicitly trying to be compatible with it. All the ARM inline assembly I've written, targetting GCC backends, has been ARM syntax, and likewise for all the disassembly output.
The DSPs and DSP-likes are... always weird. So I try to stay away from them and make them someone else's problem. But I don't think they use AT&T syntax either. It doesn't work so well for truly strange processors anyway.
I'm one of those guys who tends to have the makefiles output the disassembly, and have it open on the other monitor while I'm working, so I'd notice if it were different....
1. It’s the syntax in the manual. The last thing I want to do when reading or writing asm is to mentally translate from the manual to AT&T syntax.
2. Addressing like (%rax) is tolerably. But the AT&T scale * index + offset syntax is inexcusable. Give me the verbose Intel addressing syntax any day, please.
(As a kernel programmer, I’m more familiar with AT&T. I still hate it. I’m morbidly curious how Intel syntax ought to handle things like SGDT. Maybe SGDT SIXBYTE PTR [address]? The fact that four bytes is called a DWORD isn’t great.)
Right! I worked on C++ compilers for years and I don't even know where is the canonical book of AT&T mnemonics. At the rate that Intel is adding new instructions, using anything other than their official docs (and thus their official names) seems nuts.
However, I recognize that it's objectively worse for a human coder and contains some syntactic footguns which shoot even a experienced coder regularly:
- `number` (memory displacement/pointer) used instead of literal `$number`. It's an easy automatic mistake even for a person who knows this very well.
- the SIB clauses for x86 (memory addressing with constant Displacement and Scale and Index, Base in registers) look like `D(B, I, S)`: it's possible to remember this, but reading/writing it is not as obvious as `[D + B + I * S]`.
- Intel syntax in general is more similar to high-level languages, even though it's more verbose.
- AT&T syntax has syntactic redundancies like '%' before each register which make code much noisier than needed.
In practice, though, naming a symbol with a register name is error-prone and confusing. I'd rather have a way to escape a symbol when it's really needed than to pay the price of noise for an admittedly bad idea.
This allows for unusual languages like LISP and FORTH, without mangling the symbols. Symbols could have commas and spaces.
Keeping tricks like that stable and clearly documented is, uh, not for the faint of heart.
That's not to say that the Rust team should have tried to find (or create) an alternative; it's outside their core mission, and choosing between popular existing alternatives is the right framing.
Thank you for helping to validate that decision; this is the kind of feedback we needed.
(And note that you can still choose to use AT&T syntax, it just isn't the default.)
That said, this is a good decision because C compilers seem to be the major holdouts at this point—binutils has had .intel_syntax for a long time now, it’s just not supported inline.
Microsoft's assembler uses Intel syntax, and their inline assembly also uses Intel syntax.
Their inline assembly is much less explicit about inputs, outputs, and clobbers, and "just" has you mix C symbols and labels with assembly instructions, which I imagine is a pain to get right.
However Microsoft has de-emphasized inline assembly and doesn't allow it on amd64 or ARM. For things that need specific instructions you need to either use intrinsics or put assembly in a different object file.
That's just sad :(
Examples: An abort call on an embedded system that needs to disable interrupts to prevent task switching after the abort. One instruction.
An implementation of _start for embedded that can do everything in C except setting a couple processor flags. Two instructions.
Running (with a lock) event callbacks in an embedded system on their own stack, so that not all tasks need to have their stack big enough to handle the stuff the callbacks might do; 5 instructions that are far better inlined than having another call and indirection. (In addition to keeping doc/code together).
But the way they had it, they presented a seamless blend of assembly instructions and any C identifier, in or out, and I guess the compiler would need to parse out any side effects and cope with them. My guess is they looked at porting all that to ia64 [which they supported until Server 2008 R2], amd64 and ARM and balked.
No idea if they ever had it on some of the old architectures they supported in NT4 days or on CE (alpha, mips, ppc).
add.s.ne
ldm.ia.cc
(in both orders, for those cases with two suffixes)
asm(".intel_syntax noprefix;"
"xor eax, eax;"
"xor edx, edx;"
"1:;"
"mov r8, [rdi];"
"mov r9, [rsi];"
"sub ecx, 64;"
"jl 2f;"
"cmp r8, r9;"
"jnz 3f;"
"lea rdi, [rdi - 8];"
"lea rsi, [rsi - 8];"
"jmp 1b;"
"2:;"
"not ecx;"
"shr r8, 1;"
"shr r9, 1;"
"shr r8, cl;"
"shr r9, cl;"
"cmp r8, r9;"
"3:\n"
"seta al;"
"setb dl;"
"sub eax, edx;"
".att_syntax prefix;"
: "=&D" (d0), "=&S" (d1), "=&d" (d2), "=&c" (d3), "=&a" (cmp)
: "0" (l), "1" (r), "3" (nr_key_bits)
: "r8", "r9", "cc", "memory");Why there is always a guy who doesn't like the default and unaware that it's just one of the options?
Wouldn't be nice to have a way to embed other languages without the need for those quotes everywhere?
I guess it is meant to avoid complicating the syntax/grammar reusing the existing macro one?
Token trees allow a very wide set of syntaxes. rust-c for example allows you to embed inline C code as token tree via a macro: https://github.com/lemonrock/rust-c
Buy using a raw string literal you have a well defined quoting syntax mediating the two languages.
https://ziglang.org/documentation/master/#Global-Assembly
The "magic" being that lines beginning with double-backslashes are string literals.
> The "magic" being that lines beginning with double-backslashes are string literals.
So… strictly worse than rust which allows linebreaks inside string literals, and thus only require a dquote before and one after the entire block instead of a two-char prefix before each line?
In practice, inline assembly has pretty much been seen as a "paste this text into the generated assembly of the program, after substituting some values determined by register allocation"--in other words, it's a pretty natural string formatting problem, and describing the language like you would a formatted string is a natural step to take.
Another point to make is that assembly languages are wildly divergent in their actual syntax. ARM uses # to denote literals, x86 (Intel syntax) uses # to denote comments.
Scala originally supported "inline" XML but moved it into some form of compiler plugin you can opt-in to as just having it enabled had drawbacks even if you don't use any XML... (I don't remember the exact drawbacks anymore).
For example, in most languages including Rust, parentheses are balanced as a rule, but they might not be in some inner language. So if you want to define a syntax where you can say
inner_language!( smiley = :-) );
while you can certainly write a parser that understands what's going on there, any existing Rust parser will get very confused. So, unless you're confident the inner language is syntactically compatible (even if it's meaningless) with the outer language, you'd need to protect it somehow.Making the inner language quoted means that existing parsers that already know how to handle the outer language's string syntax (where to find the close quote, how close quotes might be escape, etc.) work seamlessly. You could also define some other syntax for it, but it's fundamentally the same requirement - it's a well-defined open and close syntax - and most other syntaxes end up uglier (think of <script>//<!-- ... //--></script or <script>//<![CDATA[ ... //]]></script> in HTML/XHTML.)
And there's nothing preventing a particularly clever parser from recognizing the asm! macro specifically and syntax-highlighting (or otherwise tokenizing/processing/etc.) the text inside, if it wants to.
https://dlang.org/spec/iasm.html
No quotes required
I hope you can see why I prefer that. Even though the FB compiler is not very smart and just pushes and pops all important regs to and from the stack its asm syntax is a joy to write. I don't have to specify clobber registers, in, out and variables and functions are in scope by default. Also the asm code is not a string and doesn't need to be quoted, it's doesn't look like something foreign at all.
I know that something like that is currently not possible in Rust but I'd love to see it some day.
[1] https://www.freebasic-portal.de/befehlsreferenz/inline-assem...
Feel free to propose a better syntax, then. This feature is not yet part of Stable Rust, so syntax improvements are very much possible.
The FreeBASIC syntax seems problematic though, since it would require architecture-specific smarts in the compiler and make it hard to port it to different architectures or add support for newer asm features. It might be better to have these additional features as separate tooling or crates, that might even wrap the asm! syntax itself via proc macros.
So better syntaxes are possible, as long as they can be built on top of the proposed inline-assembly feature.
They can, however, just be implemented as normal Rust libraries, and do not need to be part of the compiler.
EDIT: Just to be clear it's still important. But it's mainly used for a view "very high control" use cases.
A lot of the improvements you've suggested have been suggested in the past, but there's some reasons why they aren't being proposed. In short, sufficiently smart compilers can probably guess at a lot of constraints automatically, but building that level of smarts is very daunting, and for most use cases, the user probably wants/needs the fine-grained control anyways.
For example, take the call instruction. Which registers should the compiler mark that as using and/or clobbering? It depends on the ABI of the function being called--and the user might not even be calling a function at all (say, using a call as a gadget to get the current program counter). Saving a register is another common technique in inline assembly, and you don't want the compiler to infer that the register is clobbered--indeed you're saving it to explicitly not clobber it.
So how does the compiler know that these are clobbered then ?
As the post mentions, inline assembly comes in handy in a number of low level contexts (e.g. dealing with memory-mapped devices, working on microcontrollers, using processor features that aren't exposed by the kernel or standard library).
As someone who doesn't do any low-level work, could you give an example of these kinds of features? I'd like to read into some of them :)
Or try to program a micro-controler "hello world" that actuates an LED ?
The most widespread cases of this (SSE, AVX2) came from Intel trying to get lock-in in the high-performance computing market. So it took a while for AMD to sell chips that implemented these instructions and even Intel's catalog doesn't uniformly offer them.
It's also tough to get the compiler to emit these instructions where you want even if it knows they're available (unsurprisingly, since you're asking it to auto parallelize a computation), so in the high-performance space a lot of people just have to resort to inline assembly.
What you are describing, with special instructions, is called Single Instruction Multiple Data (SIMD), or simply vector instructions.
ILP means the execution of multiple separate instructions in parallel per clock cycle, though techniques like superscalar, out-of-order and speculative execution. ILP is sometimes used as a measure: How many instructions per cycle can be issued. It does not require special instructions, it's just the hardware being clever about running existing instructions faster.
We talked about Instruction Level Parallelism vs. Thread Level Parallelism vs. Node Level Parallelism since generally if you are working on a problem with some data dependency you will really have to think about each separately to get the best performance.
Some C/C++ compilers will expose these instructions as intrinsics, but AFAIK Rust doesn't.
Or kernel features not exposed by the libc.
Rust already has unsafe blocks (explicitly marked `unsafe`) for this kind of situations. Inline assembly obviously is allowed only in unsafe contexts and should be used very carefully.
Rust is not designed for memory safety only. If you only want that, there are other simpler options, like any functional, scripting or managed language. Instead, Rust is designed to bring as much memory safety as possible (but not more!) to the low-level and performance fields.
- Operating systems. For example, the voodoo that happens during early boot, where you need to walk the CPU through different modes. And also fundamental kernel stuff like mapping memory.
- Cryptography. Crypto code is often hand-written in assembly for efficiency. (Sometimes it's even the other way around: crypto algorithms may be designed with specific machine instructions in mind.) Also there are places where you have to guarantee that some operation happens in constant time, which is hard to do when a compiler may choose to insert branches or jumps without telling you.
To compete with C in those spaces, Rust will need inline assembly features.
As far as safety is concerned, yes, inline assembly is completely unsafe. In that sense, it's not so different from calling functions in the C standard library (or any other C library), which might do absolutely anything with your memory. In all of these cases, the Rust programmer has to use the `unsafe` keyword, and it's up to them to make sure that Rust's rules are still respected after the unsafe code has run. Doing this properly, and wrapping it all in a safe Rust API, means that other safe code can then use your library without the `unsafe` keyword and without any risk of triggering undefined behavior.
Isn't every system call an assembly instruction? Not just the voodoo stuff?
My only experience with "real" assembly was my OS class in college, in which a project we had involved adding system calls to Linux, and they were all snippets of assembly code called in C.
For the weirder cases like right after boot or handling an interrupt you generally just have to go full assembly for that, since in general you're not started out in a state where you can just start running your typical C/Rust/etc. code. It depends heavily on your platform at that point though.
`unsafe` occurs in rust source code where the author of some code needs to do things in a way that can't be directly proven to the compiler to be safe. These include things like calling C functions and ASM code (both cases where we can't infer all the information necessary to ensure safety). The author of the unsafe code then provides an "safe" abstraction around the unsafety that ensures that when one uses the "safe" interface, no undefined behavior occurs.
At the lowest level there is always some unsafety: system calls, libc function invocations, asm, modifying various memory mapped registers. What rust provides vs C or C++ is effective isolation of the unsafety.
So technically it does break some code, but only code which was deliberately opting into unstable features with the knowledge that it may break in the future.
Actually, with regards to asm!, it's been known for a long time that it was unlikely to be stabilized as-is (being originally a very thin wrapper around LLVM's inline asm functionality). Probably nearly everyone who's been relying on inline asm to make their code work has already been following the effort to replace asm! with a better version.
Except for my colleague who basically says "nah, they'll never be able to move off the LLVM syntax, the code i'm writing now will be supported forever".
Fortunately, we only have a small amount of assembly, so if we ever do want to upgrade to a version of the compiler which has dropped support, it won't be a lot of work.
I think it's safe to infer from this line that there was not yet a guarantee of stability in the interface, and that breaking backwards compatibility was therefore fine.
>We renamed the existing asm! to llvm_asm!
They also kept the existing functionality under a different name to ease transition. Current users will need only update the macro name and then transition to the better syntax at their own pace.
You need to deliberately opt in to unstable features, so you are unlikely to use something like this by accident.
Let's say you are browsing my website. Suddenly, your browser gets an update and prompts you to re-open the window. My site no longer displays correctly due to a backward incompatible change.
My site is the web and your browser broke it.
If you use a native app or service I write in rust 1.35 or whatever and there is a backwards incompatible change in a new version of rust it has no impact on your use of that app at all.
It's one of the reasons Java is so widely used, you can still use Java 1.4 libraries in Java 13 and most 1.4 code will keep working and compiling.
left-pad flashbacks intensify
The first is the `stable`/`nightly` system. Anything in `stable` gets compatibility guarantees, anything in `nightly` can change or be removed at any time.
Nightly also has feature-gates: using an unstable feature requires an opt-in to enable it. The old `asm!` syntax which got renamed was only ever in `nightly`.
The compatibility guarantees for `stable` are based on editions: each year there's an edition, your project's `cargo.toml` specifies which edition your code is for, and compilers will continue to support old edition code. New editions might require changes to existing code, but as long as you don't update the edition in your `cargo.toml` you can freely update your compiler (to a new stable version) without worrying about it breaking. You can use crates (packages) from any edition with any other edition, as long as the compiler is new enough to support the latest edition used.
C is of course higher level than assembly and I understand that C is very much not “safe”, so it makes me wonder whether this gives library developers incentives to add code that has a higher likelihood of creating security bugs while optimizing their code.
Is there any reason to believe this could be the case, or am I just misunderstanding how all of this fits together?
It does strike me as weird that there aren't any popular resources that sit between something as high-level as the linked e-book and something as low-level as "buy some hardware and figure it out".
There are simulators for teaching assembly, but I don't think those are nearly as fulfilling as hacking on real hardware. And since it's difficult to hack on your computer's hardware, you're left pickup up something more simple.
You can still get that feeling on x86. For instance, try writing some code directly in the MBR of a disk that writes some bytes to the serial port on 0x3f8 with outb, and load it up in KVM.
It apparently is, given that D only supports x86 assembly, doesn't support all instructions and has idiosyncrasies like requiring prefixes to be written as separate instructions.
It can be fun to try and hand-optimize code in regular applications. I can't help but feel, there's more juice in teaching the compiler to recognize the case and generate better code instead. But few know how to go down that road.
As for multidimensional array traits, you can use Index<(usize, usize, usize), Output = f32>.
Basically from my point of view the changes are about adding features (inline ASM in this case, async I/O previously) but it doesn't really change the way you code, it's just an other tool in your belt that you may decide to use later. If you didn't have a need for inline ASM before you still won't be using it, if you couldn't use Rust previously because of bad ASM support it opens up new use cases.
That's a sadly common misconception. The unsafe blocks only enable a few extra language features, like dereferencing raw pointers and calling unsafe functions; if none of these extra language features are used, the code is completely safe. That is, if you have a block of code ({ X }) and just add the unsafe keyword (unsafe { X }), it works exactly the same and is still safe. This also shows that there are not "two languages"; what you called "rust" is a subset of what you called "unsafe rust", not a different language.
> However, Rust has a second language hidden inside it that doesn’t enforce these memory safety guarantees: it’s called unsafe Rust and works just like regular Rust, but gives us extra superpowers.
I disagree that “{ X }” and “unsafe { X }” are the same. The former is provably safe if a bug free rust compiler accepts it. The latter leaves the burden of proof on the programmer.
Also people doing cryptography things occasionally need access to specific ASM instructions for their work. I believe AES has specialized instructions, but there are more cases.
You're not wrong that writing inline ASM is a fairly specialized need, but for those who require it this will come in incredibly useful.
[1]: I still need to try using this, so I might be wrong. I was waiting on inline assembly to be available before trying tho, so quite excited it's now available.
https://doc.rust-lang.org/core/intrinsics/fn.ctpop.html
https://doc.rust-lang.org/std/?search=count_ones
There are still use cases for raw assembly untouched by the optimizer - e.g. constant time cryptography functions designed to thwart timing attacks might desire control over the exact instruction stream. Although, most of that can be tackled with out-of-line assembly - as one must do with x64 MSVC for example.
Just within the rust world, https://www.nickwilcox.com/blog/autovec2/ from a few weeks ago talks about the limitations with the compiler around vectorisation, demonstrating that with hand-rolled assembly they were able to cut the runtime in half, vs what the compiler was able to produce.
Outside of the rust world, you'll often see hand-rolled assembly in performance critical code across a spectrum of uses, e.g. OpenSSL uses it extensively for performance reasons, Go has it in a number of places in their libraries, with a pure go fall-back, and so on.
edit: Thinking about it, that's not true. I was dealing with some hot code running on some MIPS architecture stuff a while back and the compiler's generated ASM was merely "okay", for the standard library. Alternative libraries with hand-rolled ASM ran significantly faster and we were going to leverage those, when another team came up with architectural changes that allowed us to take that away from the MIPS part of the stack.
For example, I've been working on a big integer library. Addition is a bit annoying, because you need to add 3 numbers (the prior carry bit, and the two digits you're adding) and get out a new carry bit and a resulting digit. This is a bit cumbersome:
let (res, carry1) = target_digit.overflowing_add(carry as u64);
let (res, carry2) = res.overflowing_add(other_digit);
*target_digit = res;
carry = carry1 || carry2;
The resulting assembly is a fairly literal translation. We perform the addition using an add instruction, and extract the carry flag into a register using setb. addq (%rdi,%rsi,8), %rcx
setb %r10b
addq (%rdx,%rsi,8), %rcx
setb %al
movq %rcx, (%rdi,%rsi,8)
orb %r10b, %al
movzbl %al, %eax
But there's a dedicated instruction for this, adc. adc adds two operands and the carry flag, while itself setting a carry flag. Manually unrolling the loop a bit, I wrote this assembly: shlb $8, {carry}
adcq 0x00({y0}), {x0}
adcq 0x08({y0}), {x1}
adcq 0x10({y0}), {x2}
adcq 0x18({y0}), {x3}
adcq 0x20({y0}), {x4}
adcq 0x28({y0}), {x5}
setb {carry}
And got a 3x speedup.I assume you've been using Compiler Explorer a lot?
Defined, by default checked in debug mode and unchecked in release, unsigned wraps and signed wraps as two's complement. This can be overridden by explicitly setting overflow-checks in the relevant profile.
It also, separately, has explicitly wrapping, saturating and checking versions of basic arithmetic operations.
I've found it very difficult to provoke rustc into emitting an ADC: the only case where it does so AFAICT is when adding u128s, which are implemented using u64s. Not sure why, except that the shortest function I could think of to emulate ADC is kind of baroque, and it's possible the compiler can't figure it out.
I've mostly been using cpuprofiler[1] and Vtune to simultaneously profile my code and show the assembly. In theory they both provide timing information per-instruction, but I don't really trust it. For the 6 adc instructions above, it shows the number of clock ticks as ranging from 22 million to 3 billion, which doesn't make sense to me. But at least it shows me the assembly!
[1] https://docs.rs/cpuprofiler/0.0.4/cpuprofiler/index.html
https://news.ycombinator.com/item?id=23351007
specifically recommends against the carry variants of addition, because the instructions are still dependent on each other and don't pipeline well. In other words, it's using the same algorithm, just buried in a single instruction, and that doesn't necessarily make it faster.
Have you considered using a strategy similar to what the article suggests? I think the HN comments also had some additional suggestions.
The approach in the article only works if you are adding a lot of numbers together, and then indeed doing carry propagation once at the end is obviously faster.
But of course there is no way that doing the carry propagation yourself on one addition can possibly be faster on a decent CPU that implements add-with-carry efficienly.
That certainly sounds useful for, e.g., totaling an array of big integers.
Did you miss the "And got a 3x speedup" part of the post you were replying to? Actual benchmarks of real code always trump theoretical deliberations.
A big integer library may have many use cases; a benchmark only shows one data point. It's possible that by deferring carry work across more operations he'd see an even bigger improvement.
Multiplication initially seemed like a very promising use case, since it's basically repeated addition. But I'm not super optimistic about that, because I think it's dominated by the alternate optimization of noticing that the product of two 64 bit numbers cannot saturate the high 64 bits of the resulting 128 bit number, which causes carries to be bounded [1].
[1] https://github.com/rocurley/bignum/blob/b45448a156fb9100ab06...
There are literally tens of thousands of bugs in the GCC and LLVM/Clang bug trackers about that.
I don't remember the last time I read some compiler-generated asm and thought "wow, that's great code".
Compilers almost always write better code than most humans.
Compilers are programmed by humans. You are claiming that somehow compilers have learned to produce better assembly than the humans that programmed them.
I am a compiler writer, and know many compiler writers. I have yet to find one that would stand by your claim. When I look at the output of LLVM or GCC, I am ashamed.
I wish I had more time to make LLVM much better, but actually, 99% of my time is spent in fixing bugs filled by users that are neither compiler writers nor assembly programmers, and they too realized how bad the machine code emitted by these tools is.
So sure, a compiler emits better assembly than a human that hasn't learn assembly programming. It also emits better assembly than all humans that do not know how to program.
But no, it does not produce _by far_ better assembly than any programmer that actually cares about machine code enough to just take a look at it and check it.
I guess it leaves PC based languages to have actual inline Assembly parsers or intrisics.
I can see why they would not want to bless a particular syntax tree or whatever into the core compiler.
Don't get me wrong: Parsed assembly is superior because you can have certain parser guarantees/checks. But it will definitely limit the scope of your language or at least slow its adoption to new platforms.
They are free to go elsewhere more welcoming.
But related: There's no reason parsers cannot be externalized and inject the resultant assembly strings into the Rust solution. If that's really what people want. The nice thing about external parsers is that they become optional in every sense.
If the Rust team builds them in, then every supported platform needs support, every development environment needs every parser, and tooling also has to support it (e.g. IDEs, code checkers, etc).
It isn't simply just adding it once and leaving it forever, the work is repeated several times over and the maintenance will be time-consuming for what is: A niche feature.
Productivity matters everywhere, systems languages that care about happy programmers have better chances of long term adoption.
It also ignores the reality that the Rust team's time is finite, and if they invest it into three or more assembly parsers then they aren't investing it into other areas of the language that could also be productivity multipliers. Areas that more programmers actually use.
Ultimately how much is unsafe assembly in Rust going to be used? And how large are those code sections? And are you better off creating Assembly in existing dedicated tooling and importing it, even if Rust had native parsers (e.g. due to better debugging/visualization elsewhere)?
Honestly if Rust code outside of standard libraries ends up with a ton of assembly anyway then that's a weakness of Rust. You should be using as little as possible. Even unsafe Rust is superior to assembly because it is platform agnostic. I'd go as far as to call it a "last resort."
Niche is about the environment, i.e. the problem domain.
Analogy to biology/ecology, grazing on mountain tops is a niche that is also unpopular (i.e. low usage), meanwhile pollination is a niche that is popular.
The problem domain for Rust to be useful is very large, across the programming ecosystem and across the programming ecosystem effort is put into increase accessibility for the less and less experienced (e.g. embedded, wasm, webservers, clis, etc). However, Rust might currently lack popularity (i.e. low usage).
The problem domain for Inline Asm to be useful is very small, typically by experts in narrow domains and across the programming ecosystem effort is put into decrease the need for inline asm (e.g. crypto, firmware, drivers, etc). It also happens to be unpopular, and is avoided as much as possible.
Summary, Rust is pragmatically niche, rather than theoretically niche, Inline Asm is theoretically niche, and also pragmatically niche.
That being said, it wouldn't be hard to create an asm_x86! macro that hides the string.
Architectures are weird, assembly is crazy. There just isn't going to be a single set of semantics around what you want.
This Rust thing isn't revolutionary, it's indeed just a prettier way of writing the model GCC invented a few decades back:
+ "Assembly" is a black box for the code author.
+ The compiler's job is generating code around the assembly, not the code itself.
+ So the fundamental model is "constraints": the rules the compiler needs to follow before and after the assembly, and how to emit an appropriate string to represent the register/address/immediate/whatever requested by the assembly code.
And like it or not, this works. It works really well. Generations of OSes have been written in this model without much complaint (beyond the general ugliness of the gcc syntax, which Rust is trying to fix).
I think this is fine. I agree it's not going to bring a revolution, but we really don't want one in this space.
Amiga and Atari also had similar inline Assembly support.
GCC invented "constraint metalanguage as the foundation for inline assembly generation", which is the model here that enables one compiler and one syntax to work the same way for basically every architecture over decades. Modelling the interior of the assembly code as a black box (which in this implementation means "string") is just a side effect of this important design choice.
Again, the stuff you're imagining isn't a general purpose assembly language model, it's an x86-specific hack that works only on the specific compilers and architectures for which it was targeted.
They interact with the backend.
They weren't specific of x86, although I mentioned only PC, they were available across all 8 and 16 bit home computers and several embedded targets support it as well.
If you want to use a single specific instruction or two, it's usually better to just use the compiler's intrinsics (Rust has already stabilized the x86 SSE and AVX intrinsics) than inline assembly. When you go to more complex assembly fragments, you tend to need more control over how the inline assembly fragment interacts with scheduling and the register allocator, and making that control work ergonomically is that much more challenging.
On the rare occasion I write inline assembly, my annoyances were autoindent, syntax highlighting, and keystrokes required. Only the latter is directly caused by the stringy inline syntax, and an editor could still mitigate it.