5
$\begingroup$

In my programming language that compiles to WebAssembly, I am using the following syntax for inline assembly:

Function sqrt(Decimal32 x) Which Returns Decimal32 Does {
  If USE_WEBASSEMBLY_SQRT_INSTRUCTION Then {
    Return asm_f32(
        R"multiline((f32.sqrt
    (f32.load
        %x ;;The compiler will replace "%x" with assembly code representing a pointer to the variable "x".
    )
))multiline");
  }
  EndIf;

So, I am using C++-like multiline strings, and you can reference all variables in the scope within inline assembly by prefixing them with % (so, there is no way to limit which variables the inline assembly has access to and whether some should be read-only).

So, what are some other approaches to inline assembly? What are their pros and cons?

$\endgroup$
0

3 Answers 3

4
$\begingroup$

The options I've seen are:

  1. Inline asm with dedicated syntax.
  2. Variants of text based asm.
  3. Intrinsics
  4. Inline asm with uniform syntax.

(1) is usually only feasible if there is a limited amount of processors you're writing the compiler for, as following asm conventions for each processor is essentially parsing a separate language. Repeating that for every processor takes a lot of effort. In addition, if this inline asm isn't smart enough in detecting clobbers etc, then it will generate worse asm than something like (2). Since inline asm is only used when perf is paramount, it's a bad idea to have it perform badly.

(2) This is what GCC/Clang does: slap some text in there to just shuffle away to the assembler. Typically there are cryptic ways to indicate how to pass data in and out of the asm block (the constraints). Rust and others are looking at making those constraints a little less cryptic, while keeping the whole thing with asm passed as text. The downside is that text just isn't a great solution. If you have asm in your language through text, you should really do verification or it will break at codegen if you use something like LLVM. But verifying it is quite a lot of work so...

(3) This is what Microsoft picked for x64 (after using 1 previously). Intrinsics work well with the compiler if the compiler understands the intrinsics. So good optimization and it's more high level than asm, making it easier to use them cross platform. That said, it's a lot less control than asm. The way I hear it people in general prefer (1) and (2) over intrinsics.

(4) I haven't seen this much. I know some fledgling languages are doing this, including my own C3 language. The idea is to have a sufficiently simple grammar for the asm so that "all" types of asm fit the same grammar. The frontend then knows clobbers etc for all instructions and generate valid (checked) text asm for the backend (alternatively the machine code directly for custom backends). The advantage is that each processor type is reduced to a set of instruction names and corresponding argument patterns and clobbers. It's more work than (2) if no text analysis is done, but it's less work than (1) to implement and support different targets.

$\endgroup$
6
$\begingroup$

In Rust, inline assembly is introduced through the asm! macro. The instructions are specified as separate strings with variables represented as {foo} within the instructions, and then defined in the end along with their storage classes and linked Rust variables.

Here's the example from the linked docs:

use std::arch::asm;

// Multiply x by 6 using shifts and adds
let mut x: u64 = 4;
unsafe {
    asm!(
        "mov {tmp}, {x}",
        "shl {tmp}, 1",
        "shl {x}, 2",
        "add {x}, {tmp}",
        x = inout(reg) x,
        tmp = out(reg) _,
    );
}
assert_eq!(x, 4 * 6);
$\endgroup$
6
  • $\begingroup$ An actual optimizer uses lea, not shl :P $\endgroup$
    – Bbrk24
    Commented Jun 28, 2023 at 12:38
  • 1
    $\begingroup$ @Bbrk24 The example is taken as is from the rust docs - you should submit this as an issue to them. Although I suspect this optimization is probably carried out at a later stage and likely affects the inline assembly as well $\endgroup$
    – abel1502
    Commented Jun 28, 2023 at 12:56
  • $\begingroup$ Inline assembler tends to be for situations when you know something that the compiler doesn't, so the optimiser might be well advised not to touch this. $\endgroup$
    – gnasher729
    Commented Jul 28, 2023 at 11:40
  • $\begingroup$ @gnasher729 I know that in C++ inline assembly is optimized by default, unless you prefix it with volatile. Since (most) optimizations are conservative, there's no harm in doing them unless there's some hardware-specific side effects of your code, which the compiler really has no clue of $\endgroup$
    – abel1502
    Commented Jul 28, 2023 at 13:22
  • 1
    $\begingroup$ @TobySpeight Fair point - I'm referring to gcc and clang as the de-facto standard. I've actually just checked the docs, and MSVC explicitly states they never optimize inline assembly, so this might be a less common approach than I believed $\endgroup$
    – abel1502
    Commented Jul 28, 2023 at 15:57
1
$\begingroup$

You want to keep it simple for the application developer.

The best IMHO is "intrinsics". Usually I don't really want to use assembler, what I want is to use a feature of the processor that isn't available from a high level language. Intrinsics are used very similar to ordinary functions. The compiler knows what these functions do and how to translate them to machine code, just as it knows how to translate + - etc. into machine code. If it generates code for different machines, it knows how to do this, so I can use the same intrinsic on PowerPC, x86, or ARM without any changes to my own code.

For example processors tend to have an instruction that counts the number of bits that are set in a 64 bit integer. Many compilers have an intrinsic "popcount" which returns the number of bits that are set. On any machine. Integrated into the optimisation process. And the compiler can translate popcount (100) -> 3 because 100 = 0x40 + 0x20 + 0x04. It can remove the instructions from loops, can move it around to the most efficient place and so on. And of course it's easy to use, and my code stays readable.

And it's very easy to learn. Every time I use a different method, I'll have to dig out some manual or some sample code. Obviously you are restricted to things that the compiler's authors foresaw. The Rust example for multiplying by six using shift + add cannot be done with in intrinsic. (But I have seen intrinsics for rotate instead of shift instructions which are not part of any language).

Now if your code needs to run on a brand new architecture, and an intrinsic was supported on architecture A, B, and C, but not on the brand new X, then if you are lucky the intrinsic function just doesn't exist. Which means that for your brand new architecture you can write an ordinary function that does what the intrinsic does, just likely a lot slower. Take popcount() as an example: It's not hard to implement by hand, it's just not very efficient. Probably 20-30 instructions for 64 bit. And using it may be sub-optimal, but at least it works. Or there may be an instruction "approximate square root of 1/x" which is a nice building block for a very fast square root. Obviously you can just write a function that calls 1 / sqrt(x). You throw away the advantages, but at least it works. YMMV.

$\endgroup$
2
  • $\begingroup$ The downside to intrinsics comes when you're targeting a relatively new ISA - you might have to contribute the implementation for the new intrinsic if it's not already been done. Inline assembly gives greater control over the instruction sequence emitted (albeit at the cost of having to hand-optimise), so having both available is the best option, IMO. $\endgroup$ Commented Jul 28, 2023 at 14:56
  • 1
    $\begingroup$ You still need an escape hatch for unsupported intrinsics. What if intrinsics could be implemented as assembly snippets? pseudocode: #define popcount __asm__("popcount %x" : "=r" (int return) : "r" (int arg0) : "pure") $\endgroup$ Commented Jul 29, 2023 at 14:00

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .