Why is this C++ code faster than my hand-written assembly (2016)
stackoverflow.com
stackoverflow.com
I live my life by that adage
++x can be slower, depending on the context -- it introduces a sequence point.
That is virtually never the case.
> For example I try to use ++x instead of x++
Those have wildly different semantics.
Those `++` and `--` operators are something to avoid anyway.
99% of the time I don't need to worry about the profiler.
I think 100% of time is closer to truth, unless you're using ancient or embedded 8/16-bit compilers.
[1] https://en.wikipedia.org/wiki/Increment_and_decrement_operat...
Wow, B. A blast from the past, sort of. I had read a good book about BCPL (the ancestor to B) many years ago. IIRC, it was by Martin Richards, inventor of BCPL. Pretty interesting book and language. BCPL and B were both typeless languages, or languages with just one type, the machine word (16 or 32 bits, don't remember). Still I found that many algorithms and programs were expressed rather compactly in BCPL - or so it seemed to me at the time. Was quite junior then, and without exposure to more advanced programming languages - only knew BASIC and Pascal, probably; even C, I only learned a bit later.
https://en.wikipedia.org/wiki/B_(programming_language)
https://en.wikipedia.org/wiki/BCPL
https://en.wikipedia.org/wiki/Martin_Richards_(computer_scie...
Also just saw some other interesting stuff from the BCPL article above:
[ BCPL is the language in which the original hello world program was written.[3] The first MUD was also written in BCPL (MUD1).
Several operating systems were written partially or wholly in BCPL (for example, TRIPOS and the earliest versions of AmigaDOS).
BCPL was also the initial language used in the seminal Xerox PARC Alto project, the first modern personal computer; among other projects, the Bravo document preparation system was written in BCPL. ]
Even for the following trivial function:
#include <vector>
#include <string>
std::size_t foo(const std::vector<std::string>& vec)
{
std::size_t n = 0;
for(auto it = vec.begin(), end = vec.end(); it != end; ++it)
{
n += vec.size();
}
return n;
}
MSVC 2017 generates different assembly for ++it vs it++, at the highest optimization level.Just tried gcc 7.1, both post and pre-increment compile identically.
Hopefully MSVC will improve soon.
std::_Vector_const_iterator<std::_Vector_val<std::_Simple_types<std::basic_string<char,std::char_traits<char>,std::allocator<char> > > > >::operator++, COMDAT PROC
mov rax, QWORD PTR [rcx]
mov QWORD PTR [rdx], rax
add rax, 32 ; 00000020H
mov QWORD PTR [rcx], rax
mov rax, rdx
ret 0
For the ++it case: std::_Vector_const_iterator<std::_Vector_val<std::_Simple_types<std::basic_string<char,std::char_traits<char>,std::allocator<char> > > > >::operator++, COMDAT PROC
add QWORD PTR [rcx], 32 ; 00000020H
mov rax, rcx
retThat considered, both pre- and post-increment generate identical code, even with VS2017.
This matches my previous experience about pretty much any compiler in last 15 years or so -- there's no difference between ++i and i++, unless, of course, it's in a statement and changes the actual meaning of code.
"it++" case. Note that iterator function is not called.
foo PROC
mov rdx, QWORD PTR [rcx]
xor eax, eax
mov rcx, QWORD PTR [rcx+8]
cmp rdx, rcx
je SHORT $LN70@foo
mov r8, rcx
sub r8, rdx
sar r8, 5
npad 8
$LL4@foo:
add rax, r8
add rdx, 32 ; 00000020H
cmp rdx, rcx
jne SHORT $LL4@foo
$LN70@foo:
ret 0
foo ENDP
Here's the code generated for "++it" case. Iterator function is not called here either. foo PROC
mov rdx, QWORD PTR [rcx]
xor eax, eax
mov rcx, QWORD PTR [rcx+8]
cmp rdx, rcx
je SHORT $LN68@foo
mov r8, rcx
sub r8, rdx
sar r8, 5
npad 8
$LL4@foo:
add rax, r8
add rdx, 32 ; 00000020H
cmp rdx, rcx
jne SHORT $LL4@foo
$LN68@foo:
ret 0
foo ENDPWriting unreadable, micro-optimised code in the name of performance without even attempting to profile it first is another matter.
My personal rule (as a not-very-good hobbyist) is that if I have to refactor everything in order to accommodate an optimisation in a path that's already 'fast enough', or introduce some arcane hackery that only makes sense with comments into otherwise clean code, then it must be backed up with realistic benchmarks (and show a significant improvement).
My rule of thumb is if you haven't profiled it, it's premature to try and optimize it.
> but sometimes it's nice to make a habit of doing things the faster way when you have two choices that are otherwise indistinguishable.
Now this is a good habit to be in.
The context was specifically about questions on StackOverflow. You don't know if the person asking the question has profiled it or not, and the assumption is often that they haven't. Probably true more often than not, but very condescending and unhelpful to the person who has.
That might sound a little extreme, but in the past 5 years I've run into exactly 1 problem that was solved by busting out the profiler and optimizing. In that same time, I can't count on all my digits the number of features that didn't ship, estimates that were overshot, deadlines that were slipped, etc etc. I've even been part of a team that ran out of runway while popping open jsPerf to choose between !! and Boolean(). Our app was fast as hell -- too bad no one will ever get to use it.
If you're expending cycles choosing between ++x and x++ and you're not ahead of schedule, please stop.
Sorry to hear about your unsuccessful projects, that's a bummer. I hope that premature optimization wasn't a major part of the blame for any of them.
I chose that example because it didn't need a full paragraph to explain, but you're right that there are probably better ones. Edit: maybe a Python example? ''.join([a, b, c]) is faster than a + b + c, but again 99% of the time you won't be using it in performance critical code. But it's a useful idiom to know.
It’s only if you plan to concatenate a whole lot of strings that join is a clear winner.
One thing to keep in mind is that the CPU is no longer the CPU you think it is. While the x86 instruction set used to directly control the processor, today it's just a compatibility shim. Under the covers, the CPU is converting your assembly into its own microcode and it's that microcode which is doing the real work. For example, look at some of the block diagrams in section 2 of this Intel manual:
https://software.intel.com/sites/default/files/managed/a4/60...
Combine that with all kinds of other voodoo they've added--fancy caches, branch prediction, and so forth--and the "don't even try" maxim starts to make sense. I'd amend that to say "don't even try unless you're just curious."
Personally, I think assembly is a great learning tool, e.g. for understanding how pointers work, how the stack works vs. the heap, how function calls work, and so-forth. I was exposed to Motorola 68K assembly in college and it completely changed my world--lots of C++ things I considered voodoo suddenly made total sense. But I've never used assembly in production code and probably never will.
In software, it's likely that latency is not going to be a problem as long as you can get more throughput (you probably don't care if the time between when the program hits your opcode and the CPU actually executes your instruction is 1 nanosecond later than it conceivably could be as long as the number of instructions executed per millisecond is greater).
In any case, yes, you need to expose the same machine code interface, not just so people can write new software using the same code, but so that old code continues to work. It would be a nightmare if every few years there were incompatible changes to the instruction set.
And your last paragraph makes sense for why the same interface.
The other answers on that question make some good points, too.
Most instructions decode to a single internal uop already, so x86 machine code is just a compact way of programming in uops.
Things like variable-count shifts are multi-uop because of x86's legacy CISC flag-handling, but BMI2 fixes that by giving you no-flags shifts that do decode to a single uop.
You're usually not missing out on much.
Also, there's plenty of voodoo besides just translation to uops. Programming in uops directly wouldn't help you figure out why there seems to be some kind of limit on register reads per clock (http://agner.org/optimize/blog/read.php?i=415#852) for example, since the uops you'd program with would still have register renaming done to them as they're issued into the out-of-order core.
LLVM Tablegen is an approach for representation but it still requires the compiler writer to write everything down and then use it effectively and correctly. This is really hard and it doesn't get done. Instead someone ports a backend and then kinda sorta improves things until it seems better than it was.
Intel has a fine compiler which goes further. It sure isn't open source and it costs a few bucks. They're also contributing LLVM now.
The reason don't even try makes sense both for compiler writer and assembly writer is that this stuff is really hard. Also CPUs are damn fast and Intel (and ARM and ...) work really hard to solve problems dynamically (branch prediction, speculation, register renaming, caching, speculation, ...). In fact, you shouldn't even try this unless you know a lot. Agner Fog and Peter Cordes should be familiar names. But if you do know a lot, beating compilers is not difficult at all. Finding the right place to beat them? That's tough.
[1] https://www.intel.com/content/dam/www/public/us/en/documents...
Heh. I had this the other way around - picked up 68k asm for fun long before C/C++, and couldn't understand why people had such difficulty with the concepts in those.
I've never learned x86 - partly because I worry that I'd enjoy it too much and waste oodles of time "optimizing" things that don't need it. Probably useful to be able to at least read it, though.
A few weeks ago I tried my hand at optimizing some code using the vector instructions. While I was able to beat clang quite easily using assembly, I just could not beat GCC. That was sobering. There's probably some 1000 page tome I have to read to understand where I messed up in optimizing.
If you never need to optimise C or C++ code why are you using these languages? Seriously? You've almost certainly got the wrong tool for the job. (Ok sometimes someone has made a legacy decision and you're stuck with it for bad reasons. Meh).
If you don't try to understand it, you won't know things like "Never use a linked list if you care about performance at all." Even Bjarne has twigged to that one (at last!). C and C++ are languages you use when you need performance, understanding the CPU as best you are capable is crucial for performance. DO try. In fact it's insane not to work on your understanding.
Don't even try to justify a statement of "don't even try to understand". It's wrong thinking from top to bottom. Here, as everywhere. No really.
I find this "Why would you even do that?" attitude in StackOverflow to be very irritating. And it's not just in low-level optimization questions. StackOverflow should be a place for questions and answers, not a collection of lessons on corporate best practices where curiosity and experimentation is discouraged.
What I'm really trying to do is get an answer to this specific question. I'm not asking you to re-think my problem statement. Thanks for not helping.
It's impossible for anyone to just know what you're intentions are and whether you understand the deeper semantics of a problem. More often than not, people try to do things for the wrong reasons.
But as soon the asker has turned down an offer of that sort once, I really really wish people who aren't going to answer the question being asked would stay out of the conversation.
When I tried to answer these kinds of questions, I always tried to start by asking questions to make sure I did fully understand the use case and the application. In my experience, a large percentage of people, regardless of how valid their question is, refused to give enough detail to allow me to actually understand their specific use case.
After repeating that cycle several times a week for a few years, it becomes very temping to just shut down the weird esoteric crap with a canned response that might get newbies pointed in the right direction. A small handful of people will stick around to explain why they really do understand their problem and need an answer to the question they are asking. A small handful of newbies will take the advice of the canned response and learn from it. The rest were largely not going to lead to an interesting conversation no matter what.
Once (maybe 25 years ago?) I came across a book on assembly language programming for the Macintosh.
The authors wrote a circle-filling graphic routine which internally calculated the integer square root in assembly language, drawing the circle using the y = sqrt(r * r - x * x) formula!
What is more, the accompanying description of the function in book featured sentences that were boasting about how it draws a big circle in a small amount of time (like a "only" quarter of a second or some eternity of that order) because of the blazing speed of assembly language!
How could the authors not have used, say, MacPaint, and not be aware that circles and ellipses can be drawn instantaneously on the same hardware: fast enough for drag-and-drop interactive resizing?
Is a rounded rect not just a circle sliced with straight lines between the quarter circles?
Surprising that people writing a book 25 years ago would not have been aware of this work.
https://en.wikipedia.org/wiki/Digital_differential_analyzer_...
I have two-D and three-D versions of it.
I proved this back as an undergrad: i used Bresenham to plot the y = K/x hyperbolic curve.
I had this idea that since 1/x can be interpolated with Bresenham without doing division, somehow that could be applicable to the perspective transformation when walking over texture maps in 3D rendering.
Then, after getting it to work I replaced the interpolator with a bunch of assembly starting from the intermediary representation the compiler output.
I unfortunately didn't date that source file but I do remember I was living in Amstelveen when I wrote it so this was about 23 years ago, summer of '94.
We made the textures with one of the first affordable and commercially available digital cameras:
https://www.amazon.com/Graphics-Gems-Andrew-S-Glassner/dp/01...
While speed wasn't a huge issue, consistency was. Simply using sin/cos to draw the circle works, but it can lead to jagged and inconsistently placed pixels if your degree step isn't perfect. Even with my middle school level math, I realized I could just iterate over the bounding box for the circle and just use r^2 as a threshold on x^2+y^2 to determine whether a pixel should be on or off - no square root required, since it's totally redundant.
It has the bonuses of using only integer math (consistency!) and only requires slight changes to yield filled or unfilled circles. It's also almost as fast as drawing a simple filled rectangle!
The idea that the author could miss such a simple algorithm baffles me.
Jeff Tupper's GrafEq software does this kind of plotting.
Over the development of this, Jeff Tupper came up with a quine concept: a formula whose f(x,y) thresholded plot reproduces an image that can be interpreted as its math notation.
See here:
https://en.wikipedia.org/wiki/Tupper%27s_self-referential_fo...
The Devil is in that 543 digit integer. :)
(It's been submitted to HN a few times already.)
More fundamentally: it's theoretically possible to at least match compiled code performance with assembly, because you could just write the code the compiler generates.
BUT, it requires a LOT of experience.
Modern compilers "know" a lot of optimizations (e.g. integer mult by fixed constant --> shifts, adds, and subtracts). Avoiding pipeline stalls requires a lot of tedious register bookkeeping, and modern processors have very complicated execution models.
It's almost always better to start with a compiler-generated critical section and see if there are possible hand optimizations.
i.e. the part protected by a lock() / unlock() in multi-threaded code.
The usage on the context of optimization is much more deserved, although I never heard anybody calling it "section" before, just critical code, critical loop, or stuff like that.
Yeap. But once you do, it's almost always easy to get some efficiency gains, because you understand what the code intends, and the compiler does not.
Surprisingly it doesn't take much experience to do this: just a profiler, and a willingness to try different things until you find something that works.
It seems to me that the best approach to this would be to feed more information to the compiler rather than writing assembly yourself. Otherwise you give up portability.
If you do start writing assembly, you usually want to have it exist next to a higher-level version of the code that you can toggle on and off. This lets you maintain portability (just compile the higher-level version on platforms where you don't yet have assembly) and makes it easy to try out new optimizations that wouldn't require assembly.
When that's not possible, the way to keep it portable is by writing a generic C function, then writing an optimized version of the same function that will be compiled when that architecture is available.
At work it's rare to need to compile the same code for various architectures, but sometimes it happens.
On the topic of hand-optimizing or not: There always seem to be two camps. The first say: "Never bother with assembly. Let the compiler do optimizing. If anything, try to write code that the compiler can optimize.".
The second camp always states something like "It is not that hard to outperform the compiler.".
I guess the complicated pipeline models are mostly the reason for the complexity. I'm not sure how much compilers do to avoid pipeline stalls though. For example, how detailed the execution model that they use is. It shouldn't be too complicated, since most compilers (as far as I know) don't differentiate between different processors, while different processors have a different execution model.
Essentially you must have a piece of code where that constant is just a parameter to the algorithm that suddenly when set to a value of 2 makes it that much more efficient.
Now here's the bummer: if you had already planned for it, then writing custom assembly for it is not going to make the algorithm that much faster. But when you didn't - you're rewriting the whole of the algorithm anyway.
even:
mov rbx, 2
xor rdx, rdx
div rbx
the OP should've done something like even:
xor rdx, rdx % possible to remove?
shr rbx, 1 shr rax, 1
div rbx will divide the 128-bit value in (rdx, rax) by rbx, then store the quotient in rax and the remainder in rdx.It was an amazing crash-course on just how good compilers have become at optimizing. Not a single student could hand craft assembly that was faster than the compiler output. The teacher of the course was able to generate assembly that was slightly faster, and he stated that in order to do so, he had to greatly exploit his in-depth knowledge of the processor's pipeline system. That was roughly year 2000, and I'm sure compilers have only become better at their job since then.
All in all, excellent learning experience. I've since encountered several instances where developers assert superior assembly skills, and by default I'm silently skeptical of their claims.
At least for x86/amd64 - with out-of-order-exection, branch prediction and whatnot one not only has to know the architecture, but one has to know the specific implementation the code will run on. And knowledge on the deep internals of CPUs made by Intel or AMD (or Via? are they still around?) is not easy to come by.
I measured, but it did not run any faster. Damnit, this is assembly, I said, it has to be faster. So I looked at the assembly code generated by the compiler: Turned out it was pretty much identical to the code I had written. At that point, I felt brief surge of pride (because I was as clever as the compiler) and then disappointment (because I was not more clever than the compiler), and I figured trying to be smarter than the compiler was a waste of my time.
Also back in 2016, I participated in a pseudo-challenge on the fasm board [1] and it is trivial to optimise both the C/C++ as well as hand-written assembly. IMO comparing how good a compiler is at optimising is akin to how good it is at figuring out your intentions (and they all suck at that).
A very polite way of saying, "why are you even using assembly, when you don't understand assembly?"
The OP clearly does understand assembly enough to start doing project Euler type problems, which is a good way to learn basic programming in any language. They get a solution in assembler which is more than many people here would be able to do I suspect.
And they're looking to expand their knowledge by asking on stack-overflow about something they don't understand.
So why do you think they should they be met with rudeness and hostility?
tl;dr version--the author's hand-written assembly was poor.
I guess the more interesting takeaway is "Just because it's assembly doesn't mean it's good assembly."
What C and C++ give you (and Assembly gives you even more of) is control. If you can, and know how to, capitalise on that control, you _will_ get more performance. But those requirements are non-trivial.
Just because you are writing in assembler, does not mean it is going to run faster than the same code in a compiled language. There has been decades of research and who knows how many man-years of effort that has gone into producing efficient compiled code from C, C++, Fortran etc.
Your assembly skills have to be of quite a decent order to beat a modern compiler.
BTW: The answer to the question on Stack Overflow by Peter Cordes is a must-read. Brilliant.
I personally find it a shame that there are so many juicy instructions that compilers have no hope of ever using effectively. How often is a compiler smart enough to solve a problem with PDEP/PEXT, for example? Those functions are versatile as heck, but you need to plan for them if you want them to show up.
Start with compiler output, add intrinsics where you can see help is needed, benchmark, repeat.
Pathologically-slow ASM is pretty rare from modern compilers in my experience.
Here are some I've found:
https://stackoverflow.com/questions/45496987/gcc-optimizes-f... (horrific codegen for known-size C++11 loops in member functions, all GCC versions prior to 8 which is not yet released)
https://stackoverflow.com/questions/43651923/gcc-fails-to-op... (SIMD opportunity squandered when C++11 features are used)
https://stackoverflow.com/questions/42263537/gcc-sometimes-d... (failure to inline trivial operators)
https://stackoverflow.com/questions/26052640/why-does-gcc-im... (C isnan() not efficient, for many years)
But when you want to make a small change the compiler will rethink the whole function from scratch, and if you want to keep your advantage you may have to do the same.
The classic fizzbuzz will use %3 and %5 operations to test divisibility. As we know from the same source as OP, these are horrifically slow. In addition, the usual approach to fizzbuzz has an annoying duplication, either of the strings or of the predicates.
So, the challenge is, write an optimized fizzbuzz with the following properties: the state for the divisibility testing is a function with a period of 15, which can be calculated in 2 C operations. There are 3 tests for printing, each of the form 'if (...) printf("...");' where each if test is one C operation.
Good luck and have fun!
#define FIZZ 1
#define BUZZ 2
static const uint8_t divs[] =
{FIZZ+BUZZ,0,0,FIZZ,0,BUZZ,FIZZ,0,0,FIZZ,BUZZ,0,FIZZ,0,0};
static const char* strs[] = {
[0] = "%u\n",
[FIZZ] = "fizz\n",
[BUZZ] = "buzz\n",
[FIZZ+BUZZ] = "fizzbuzz\n"
};
void fizzbuzz(uint32_t upTo)
{
uint32_t i, state;
for(i = state = 0; i < upTo; i++) {
printf(strs[divs[state]], i);
if(++state == sizeof(divs) / sizeof(*divs))
state = 0;
}
No divisibility tests at allOne level of arrays may be skipped at cost of extra .rodata size (store string pointers directly in divs). But in a modern cpu cost of the extra load is small
String duplication helps speed here.
Branch misprediction will happen once every fifteen times worst case.
Plus, if shaving nanoseconds, printing is a bad idea
#define FIZZ 1
#define BUZZ 2
struct {
uint8_t flags;
uint8_t next;
} static const nfo[] = {
{FIZZ+BUZZ,1},
{0,2},
{0,3},
{FIZZ,4},
{0,5},
{BUZZ,6},
{FIZZ,7},
{0,8},
{0,9},
{FIZZ,10},
{BUZZ,11},
{0,12},
{FIZZ,13},
{0,14},
{0,0}
} __attribute__((align(YOUR_L1D_LINE_SIZE)));
static const char* strs[] = {
[0] = "%u\n",
[FIZZ] = "fizz\n",
[BUZZ] = "buzz\n",
[FIZZ+BUZZ] = "fizzbuzz\n"
};
void fizzbuzz(uint32_t upTo)
{
uint32_t i, state;
for(i = state = 0; i < upTo; i++) {
printf(strs[nfo[state].flags], i);
state = nfo[state].next;
}
}
No divisibility tests at all. No branches besides loop and printf call. Space can be saved by using a bitfield, but masking it will add speed costs. .data: 0
.rodata: 30 + 4 * sizeof(void*) + strings
.text: depending on arch, but not much
Assuming call to printf has no cost and the caches are hot, a modern x86 cpu could execute one iteration of this loop in 1 cycle (issuing 2 loads, one add, one cmp, one branch)I have no compiler and am typing this on a phone so please forgive typos, if any
Also, no, a typical x86 CPU would take about 4 or 5 cycles per iteration even if printf (and the cost of moving its arguments into the right register) was free.
state = nfo[state].next is a pointer-chasing loop-carried dependency chain, so you will bottleneck on L1D load-use latency. (For Skylake, 5 cycles for a complex addressing mode: http://www.7-cpu.com/cpu/Skylake.html).
If out-of-order execution could overlap many of these loops then the throughput could be close to 1 iter per clock.
(Also, no, `%5` isn't "horrifically" slow when it's a compile-time-constant modulus: you or the compiler can do it with a multiply and shift for the integer division, and then x%5 = x - (x/5 * 5). https://godbolt.org/g/3HwBrF. It still sucks though.)
I wrote an x86 assembly FizzBuzz for fun a while ago, intended to be an example of how to optimize (https://stackoverflow.com/a/37494090/224132). Some parts of it kind of suck, though. I made some parts efficient (like appending "buzz\n" to a buffer with a mov [rdx], r13 / add rdx, 5), but left in too many conditional branches and a function call instead of inlining everything when unrolling.
I'm glad so many people like my post that the OP linked. It was fun to write. :)
Turns out you can compute x%3 == 0 with even fewer steps, it's (x*0xaaaaaaaab) < 0x55555556 (assuming wrapping unsigned 32 bit arithmetic).
I decided to reject your conditions and replace them with my own, for no apparent reason. Mine has some of the redundancy you wish to eliminate, but the loop body is completely branchless, aside from the call to printf, of course, which we're apparently ignoring.
#include <stdio.h>
#define NUM "\x06" "%zu\n\0"
#define FIZZ "\x07" "Fizz\n\0"
#define BUZZ "\x07" "Buzz\n\0"
const char *x =
NUM
NUM
FIZZ
NUM
BUZZ
FIZZ
NUM
NUM
FIZZ
BUZZ
NUM
FIZZ
NUM
NUM
"\xa6" "FizzBuzz\n";
void fizzbuzz(size_t upTo) {
for(size_t i = 1; i <= upTo; i++) {
printf(x + 1, i);
x += *x;
}
}
int main(int argc, char **argv) {
fizzbuzz(100);
}[0]: http://blog.cdleary.com/2012/11/arm-chars-are-unsigned-by-de...
You did remove a level of indirection for the format-strings, though. You could have done that with
struct state {
int next;
char fmt[6];
};
Anyway, this has probably a 5 cycle loop-carried dependency chain on Skylake, from x += *x; compiling into a 4-cycle latency movsx rax, byte [rdi], then a 1-cycle add rdi, rax. (Or whatever registers the compiler picks).If you'd stored pointers, you could have got it down to 4 cycles on Skylake for the load-use latency of a simple addressing mode ([reg + disp] where displacement is 0..2047).
#include <stdio.h>
#include <stdint.h>
#define FIZZ(x) (x & 0x0924)
#define BUZZ(x) (x & 0x0210)
#define FIZZBUZZ(x) (x & 0x4000)
#define NONE(x) (x & 0x34cb)
void fizzbuzz(size_t n_max)
{
uint32_t x = 1;
for (size_t i = 1; i <= n_max; i++)
{
if (FIZZ(x))
printf("fizz\n");
if (BUZZ(x))
printf("buzz\n");
if (FIZZBUZZ(x))
printf("fizzbuzz\n");
if (NONE(x))
printf("%d\n", i);
x <<= 1;
x |= (x >> 15);
}
}
int main(int argc, char **argv)
{
fizzbuzz(100);
}
It's less costly than integer division, and I'm not sure it's what you had in mind. The output here still duplicates the strings.Congrats!
#include <stdio.h>
#include <stdint.h>
#define F(n) (1 << (2*(n)-2))
#define B(n) (1 << (2*(n)-1))
int main() {
uint32_t m = F(3) | F(6) | F(9) | F(12) | F(15) | B(5) | B(10) | B(15);
int i;
static char t[4][16]= { "%d\n", "Fizz\n", "Buzz\n", "FizzBuzz\n" };
for (i = 1; i <= 100; i++) {
printf(t[m&3], i);
m = (m >> 2) | (m << 28);
}
return 0;
}
Transforming that into an if-based version is left as an exercise for the reader. :)I whipped together a short poc in chezscheme, and it clocks in at about 50ms on my 4 yo laptop.
That said, I do think it's neat that the fastest brute-force variant is a rough factor-2 from your naïve memoized version. It might even win if it was fighting a hyperthread for cache space! Just shows how much throughput modern CPUs have... if only they were used this well all the time ;).
My naive memoized version in a GCd language that is consistently 4-5x slower than my own (pretty bad) C versions of the same project Euler problems without any of the tricks the fastest C++ version is doing.
Still pretty impressive considering most solutions in the project euler forums take more than 1s.
what do you mean?
Or maybe treat it as a learning exercise.
Is there any other reason to complete a Project Euler question?Compilers employ multitudes of optimizations that will go overlooked in hand-written ASM unless you, as the author, are very knowledgeable. End of story.
I'm not sure that's true. There are hundreds of compilers in the world, being maintained by thousands of developers. Then there are all the JITs. And the people who make the standard library implementations. And performance critical stuff in game engines, OS kernels, hardware drivers, high-frequency trading. Then there's the embedded space. And then there's Fabrice Bellard.
My previous employer, Cambridge Silicon Radio (one of hundreds of similar companies nobody's heard of) had dozens of people on the staff that worked on this kind of thing. I have friends at ARM, Broadcom, Samsung and Raspberry Pi that mess around with processor designs for a living. This is just my little experience of the industry. There are armies of these people.
I can't do it therefore it must be impossible!