Live data from Hacker News

Branch-free FizzBuzz in Assembly

pepijndevos.nl

11–20 of 47 posts

Re: Branch-free FizzBuzz in Assembly

#11

int ret int ret ret call call call int jmp No conditional branches. As long as you ignore what happens on the other side of those int instructions! Especially that last one...

Yea, you could probably inline the calls and unroll the loop, but I don't see a way of getting around the syscalls.

I'd be curious to know what those syscalls are doing inside.

Re: Branch-free FizzBuzz in Assembly

#12
post #4

This is equivalent code in C if anyone is interested. #include #include const char* table[] = { "%d\n" , "Fizz\n" , "Buzz\n" , "FizzBuzz\n" } ; void E( int i ) { exit( 0 ) ; } void F( int i ) { size_t c = !( i%3 ) + !( i%5 )*2 ; printf( table[c] , i ) ; } void ( *func[2] )( int ) = { F , E } ; int main( void ) { int p = 1 ; while( 1 ) { func[p/102]( p++ ) ; } return 0 ; } This of course only avoids conditional branch…

Have you looked at the generated ASM? I suspect printf contains a lot of branches. But then so might those syscalls I guess.

Yes.

Library calls will have conditionals, but apart from that there are none.

Re: Branch-free FizzBuzz in Assembly

#13
post #9
post #8

Earlier quoted context omitted.

Branch and jump are synonyms in this context.

Oh cool! I always thought of a branch as a conditional jump - I guess I was wrong. It appears that even unconditional branches are still branches! "A branch is an instruction in a computer program that may, when executed by a computer, cause the computer to begin execution of a different instruction sequence. Branch (or branching, branched) may also refer to the act of beginning execution of a different instruction s…

I wouldn't be surprised if there was some place that used "branch" to indicate conditional-only. Assembly language terminology seems to be wonderfully inconsistent. But typical usage these days seems to be as you quoted.

Re: Branch-free FizzBuzz in Assembly

#14
post #4

This is equivalent code in C if anyone is interested. #include #include const char* table[] = { "%d\n" , "Fizz\n" , "Buzz\n" , "FizzBuzz\n" } ; void E( int i ) { exit( 0 ) ; } void F( int i ) { size_t c = !( i%3 ) + !( i%5 )*2 ; printf( table[c] , i ) ; } void ( *func[2] )( int ) = { F , E } ; int main( void ) { int p = 1 ; while( 1 ) { func[p/102]( p++ ) ; } return 0 ; } This of course only avoids conditional branch…

"Unsequenced modification and access to 'p'" for:

    func[p/102]( p++ ) ;
A quick search brought me to http://www.bionoren.com/blog/2013/07/unsequenced-modificatio... : 'Basically, the compiler is free to reorder anything and everything until it hits a “sequence point”. Things like return, if, assignment, variable declaration, etc are all sequence points.'

In the parent code sample, it seems the compiler is free to execute "p++" before the "p/102" since there is no sequence point between them.

I was previously unaware of this undefined behavior. Thanks for this, rertrree!

Re: Branch-free FizzBuzz in Assembly

#16
Apparently the Rust compiler produces a slightly different branch-free FizzBuzz[1] involving this delightful snippet:

    static OUT: &'static [u8] = b"\
    1\n2\nFizz\n4\nBuzz\nFizz\n7\n8\nFizz\nBuzz\n11\nFizz\n13\n14\nFizzBuzz\n\
    16\n17\nFizz\n19\nBuzz\nFizz\n22\n23\nFizz\nBuzz\n26\nFizz\n28\n29\nFizzBuzz\n\
    31\n32\nFizz\n34\nBuzz\nFizz\n37\n38\nFizz\nBuzz\n41\nFizz\n43\n44\nFizzBuzz\n\
    46\n47\nFizz\n49\nBuzz\nFizz\n52\n53\nFizz\nBuzz\n56\nFizz\n58\n59\nFizzBuzz\n\
    61\n62\nFizz\n64\nBuzz\nFizz\n67\n68\nFizz\nBuzz\n71\nFizz\n73\n74\nFizzBuzz\n\
    76\n77\nFizz\n79\nBuzz\nFizz\n82\n83\nFizz\nBuzz\n86\nFizz\n88\n89\nFizzBuzz\n\
    91\n92\nFizz\n94\nBuzz\nFizz\n97\n98\nFizz\nBuzz\n";

    ...
1: http://chrismorgan.info/blog/rust-fizzbuzz.html

Re: Branch-free FizzBuzz in Assembly

#18
Random fizzy buzzy observation:

Always a little baffled when I see three cases (fizz, buzz, and fizzbuzz) rather than both fizz and buzz handled well such that modulo 15 gets a fizz and a buzz.

The normal fizz buzz is divisible by 3 gives a fizz, divisible by 5 gives a buzz. It so happens that 15 is both, so should exhibit both. But as I see it, handling the 15 right should be emergent behavior of a correctly written fizz and correctly written buzz, not a separate test case.

But what if the business requirement changes that divisible by 4 gives a fizz and divisible by 5 gives a buzz? Now the "3 cases" programmer doesn't just change 3 to 4, and correctly see fizzbuzz emerge on divisible by 20, he's still got a spurious fizzbuzz on the divisible by 15 case. He's got to remember this case, do some manual computation, and change his 15 to 20. Structuring the code in such a way the programmer has to track all ancillary implications of one business rule change seems a recipe for disaster.

So I consider as broken all code that explicitly prints "fizzbuzz" with a third conditional.

Re: Branch-free FizzBuzz in Assembly

#19

Random fizzy buzzy observation: Always a little baffled when I see three cases (fizz, buzz, and fizzbuzz) rather than both fizz and buzz handled well such that modulo 15 gets a fizz and a buzz. The normal fizz buzz is divisible by 3 gives a fizz, divisible by 5 gives a buzz. It so happens that 15 is both, so should exhibit both. But as I see it, handling the 15 right should be emergent behavior of a correctly written…

A good interview exercise (not necessarily vouching for fizzbuzz here) allows depth of discussion past the initial answer. It is in this discussion stage where I spend the majority of my time with a good candidate.

In such a discussion we might talk about whether it makes sense to make the /15 case "fall out" naturally. Using your example, there's an implicit assumption that the requirements would change to {4: fizz, 5: buzz, 20: fizzbuzz}, but in real life I've found things have the annoying tendency to change to, e.g. {4: fizz, 5: buzz, 15: fizzbuzz}.

Re: Branch-free FizzBuzz in Assembly

#20

Apparently the Rust compiler produces a slightly different branch-free FizzBuzz[1] involving this delightful snippet: static OUT: &'static [u8] = b"\ 1\n2\nFizz\n4\nBuzz\nFizz\n7\n8\nFizz\nBuzz\n11\nFizz\n13\n14\nFizzBuzz\n\ 16\n17\nFizz\n19\nBuzz\nFizz\n22\n23\nFizz\nBuzz\n26\nFizz\n28\n29\nFizzBuzz\n\ 31\n32\nFizz\n34\nBuzz\nFizz\n37\n38\nFizz\nBuzz\n41\nFizz\n43\n44\nFizzBuzz\n\ 46\n47\nFizz\n49\nBuzz\nFizz\n52\n5…

By my reading that implementation was assembled by humans, not the optimizer.

Although, there's certainly nothing magical that prevents the optimizer from generating such code. Here's something[1] I just threw together that (ab)uses constexpr to do just that.

On my machine, building with:

  $ clang++ -std=c++14 fizzbuzz.cpp -O2 -S
Gives code similar to:

  main:                                   # @main
          .cfi_startproc
  # BB#0:
          pushq   %rax
  .Ltmp0:
          .cfi_def_cfa_offset 16
          movl    $1, %edi
          movl    $_ZL6output, %esi
          movl    $413, %edx              # imm = 0x19D
          callq   write
          xorl    %eax, %eax
          popq    %rdx
          retq
  .Ltmp1:
          .size   main, .Ltmp1-main
          .cfi_endproc
  
          .type   _ZL6output,@object      # @_ZL6output
          .section        .rodata,"a",@progbits
  _ZL6output:
          .ascii  "1\n2\nFizz\n4\nBuzz\nFizz\n7\n8\nFizz\nBuzz\n11\nFizz\n13\n14\nFizzBuzz\n16\n17\nFizz\n19\nBuzz\nFizz\n22\n23\nFizz\nBuzz\n26\nFizz\n28\n29\nFizzBuzz\n31\n32\nFizz\n34\nBuzz\nFizz\n37\n38\nFizz\nBuzz\n41\nFizz\n43\n44\nFizzBuzz\n46\n47\nFizz\n49\nBuzz\nFizz\n52\n53\nFizz\nBuzz\n56\nFizz\n58\n59\nFizzBuzz\n61\n62\nFizz\n64\nBuzz\nFizz\n67\n68\nFizz\nBuzz\n71\nFizz\n73\n74\nFizzBuzz\n76\n77\nFizz\n79\nBuzz\nFizz\n82\n83\nFizz\nBuzz\n86\nFizz\n88\n89\nFizzBuzz\n91\n92\nFizz\n94\nBuzz\nFizz\n97\n98\nFizz\nBuzz\n"
Of course, having now written this I feel like I should retroactively fail my last interview.

[1] https://gist.github.com/anonymous/7818f902a374a953b274

Post reply on HN