Hacker Newsnew | past | comments | ask | show | jobs | submit | 14113's commentslogin

> Floating point math is often slower than integer math because the compiler is being conservative about how it optimizes your code.

It's not strictly true to say that it's "being conservative". What is more correct is to say that floating point operations have different semantics to integer operations, and an optimisation that retains the semantics of an expression over integers may not do so when applied to an expression over integers. Hence, it may be possible to apply one optimisation to an integer expression, but applying that to a floating-point expression may result in a different program meaning.

C/C++ compilers give you a way out of this with the `--ffast-math` flag, which essentially allows compilers to relax the constraints on floating-point optimisation passes.

For an example of how this works in GCC, take a look here: https://gcc.gnu.org/wiki/FloatingPointMath


> C/C++ compilers give you a way out of this with the -fast-math

People should really use the individual optimization flags they want (no signed zeros, no trapping math, associative math, reciprocal math) and not -ffast-math because the other optimizations it enables leads to surprising code (for example, isinf and isnan may become noops, which will break production code).

Basically never use an optimization flag that changes the semantics of your code without understanding exactly what that means. I have had to fix this in a number of codebases because someone thought that flag was as innocuous as -O3.

And if you have to enable flush to zero/denormals are zero it should be explicit in your code and scoped.


Yes. In fact, even if you don't ask for semantics like the -fast-math flag, the floating point types are worth some extra time to understand before relying on them.

They're much stranger than the machine integers. The machine integers are basically like the Integers you were taught in school, except for overflow. That's not nothing but it's a complexity you can ignore entirely so long as you never overflow. In Rust you can have the language keep you safe - if an overflow occurs we'll panic and we're done. However the floating point types are a weird thing entirely invented for the convenience of the machine. They're too often introduced as if, like the machine integers, they're almost familiar numbers from school. Some languages even call these types "real" - but they very much are not actually the Real numbers, not even the approximation that the machine integers were to actual Integers. The programming language can't help you cope today. You can use software like "Herbie" to help you a bit, but today's languages just leave you with it.

https://herbie.uwplse.org/

Here's an easy example you saw in school, a tenth, written 0.1 in decimal. The floating point types cannot represent this number. When you ask for the 32-bit floating point value 0.1 in a language like C or Rust, you actually get exactly 0.100000001490116119384765625 because that was a number the type can represent and it was deemed "close enough".


They're not real, they're rationals (well rationals are real but you get it).

If you want "simple" rationals, you can use the numerator/denominator scheme. This has its own problems but if you avoid overflows, they are the rationals you were taught in school, just like the integers.

The problem is that people (and languages) default to floating-point without understanding the consequences. Many times it does not matter and then floating point are indeed better (if you know how to use them, e.g. not comparing for equality), but sometimes it does.


> The problem is that people (and languages) default to floating-point without understanding the consequences.

If you were to insist on there being only one numeric data type in a language, then floating-point turns out to be the best compromise, especially because someone who doesn't understand the pitfalls of floating-point are going to be less likely to have it blow up in their face than other options. Fixed-point has a problem when the numbers have very different scales. Rational numbers don't let you do basic things like "measure the distance between two points" (because functions like sqrt or exp aren't defined on rational numbers).


> Rational numbers don't let you do basic things like "measure the distance between two points" (because functions like sqrt or exp aren't defined on rational numbers).

However the floating point types are just binary rationals, so if we took this "can't do basic things" at face value we couldn't do these operations on the floating point types either.

The reason they're so weird is a convenience to the implementation. I am not an EE so I can't tell you how much that saved, but it was a choice, obviously we can't implement the Reals because Almost All Reals aren't even Computable, but I think most software engineers really don't have an appropriate understanding of the floating point types and the result is buggy software.


The main selling point that people tout for using rational numbers over floating-point is their exactness, and the main thing people complain about floating-point is their approximate nature. Introduce the ability to do sqrt on rational numbers, and you somehow have to invent approximation for rational numbers. Choose the nearest representable rational number, and oops, nearly every operation on that rational number could overflow the numerator or denominator, so change that and oops now you've basically reinvented floating-point but worse.

Which is sort of the thing of floating-point: they may be weird, but it kind of turns out that they're ultimately less weird than all of their alternatives, if you try to do anything other than toy examples on them. It's almost like floating-point was designed by people who needed to do a lot of numerical work in computers instead of students in Programming 101!


As somebody who has implemented Hans Boehm's "Towards an API for the Real Numbers" paper I cannot agree that the floating point types are "less weird" than alternatives. They're much faster of course because they're implemented in hardware, but that's hardly surprising.

In my (unpublished) toy calculator I added a mechanism to bring an f32 or f64 into existence and convert it to my Rational type and so that's why I had the exact value of the f32 we call 0.1 to hand earlier. But of course the API goes much further than that. Want to multiply the square root of 40 by the square root of 1000? No problem that's exactly 20. A bright teenager can figure that out, and so can a properly designed API for numbers. The ordinary human activities you talked about of course work just fine in this API, it has historically been how the calculator in an Android phone works for example.

Symbolic approaches also have value in this space.

In both cases, what you do not get is the problem where the programmer mistakenly believes these types will behave like the numbers in school but actually they behave in an entirely different way. You have to go really far off piste, by which point likely the programmer has appreciated that they do not understand what's going on, before you trip on a hazard we cannot manage with this approach - whereas in traditional floating point that happens without them realising.


I mean I get that novice programmers might get tripped up on floating point representation but if you don't know "f32 can't represent 0.1 exactly" then you shouldn't yet be worried about the nuance of relaxing IEEE 754 compliance for the purposes of performance.


Yes, I was being a bit concise: Individual optimisations should be turned on as determined by profiling, application semantics, etc. My point was more that if you want to get as close as possible between integer and floating-point, then there is a flag that does it. That doesn't mean that you should do it, however...


Seconding this - having worked on LLVM and Firefox, the performance tuning of each application was very different. Even measuring the performance of an application like Firefox (in a meaningful way) is non-trivial, wheras compilers are much more approachable with traditional profilers (either tracing or sampling).


I mildly disagree - depending on your definition of "typical app". Most applications have much greater use of multi-processing and concurrent cross-thread (or cross-process) communication. Compilers, aside from high-level parallelism across modules, tend to be quite single-threaded applications.

If you're solely interested in single-core performance, then I would agree that they are a good stress test, but I think for a processor that is being sold on it's parallelism, they are not a great benchmark.


In the history of the SPEC benchmarks, the compiler benchmarks, like gcc, have been the best predictor of CPU performance for the applications that cannot benefit from array operations, so they cannot use the vector or matrix instruction set extensions.

The reason is that for the other benchmarks the CPU vendors have always succeeded sooner or later, to tweak their compilers and compiling options, or even the hardware of the CPUs, in order to get improved benchmark results that nonetheless are not indicative of the improvements in other applications.

On the other hand, the compiling benchmarks, like with gcc, and now also with clang, are too diverse in CPU resource usage and no special feature of the CPU has a significantly greater weight than others, so special tricks to enhance the benchmark results have never been found.

When looking at the past SPECint results, the values of the gcc benchmark remain the most reliable relative performance estimator.

I doubt that this will change in the near future.

Moreover, the multi-threaded compiling benchmark is also very useful, because it matches exactly a real-world workload that is extremely frequently encountered. Due to the great clock frequency difference between running a benchmark on a single thread and running it on all available threads, the single-threaded results have a very poor correlation with the multi-threaded results.


> I feel like this is with 2026 view where browsers are so mutually compatible.

I wish this was the case. Unfortunately, companies that work on non-Chromium browsers need to employ dedicated web compatibility teams to either a) help website users fix non-standard (i.e. Chrome only) HTML/CSS/JS, or b) replicate Chromium-like behaviour for specific (very popular) websites so that they work "correctly".

There's also the websites that deliberately block certain browsers which is what tools like "chrome-mask"[1] are built to solve.

[1] https://addons.mozilla.org/en-GB/firefox/addon/chrome-mask/


There are two types of people. Those who can extrapolate from incomplete data, and


…those who understand binary? ;)


As I experienced while trying to write out an AST for this pattern, the operator precedence makes it harder to read. I would at least prefer that it's written as *(foo++).


There are some badly chosen operator priorities in the C programming language that can make expressions without parentheses harder to read (e.g. for the bitwise operators), but the fact that postfix operators are executed before prefix operators is a very simple rule that is hard to forget, so for me adding such superfluous parentheses makes the expression harder to read, not easier to read.


To me, the parentheses make it significantly easier to read. They make it clear which operator directly acts on the variable, and create a mental meta-object to which the next operator acts.

I accept that if you're extremely used to writing this style of C code, it might be something that you're used to, and understand implicitly. As a C++ engineer that infrequently comes across this precise pattern, having the precedence made explicit makes it much easier to understand.


Representing code in a compiler is not precisely trivial, and the two statements are actually quite different from a compiler or AST perspective. Just looking at the first branch:

  *lwr = x; lwr++;
This could be be represented with something like this (and this is a very vague approximation of an AST):

  block
    statement (assignment)
      expression
        operator (dereference)
          variable
      expression
        variable
    statement
      expression
        operator (post-increment)
          variable
The second form, that looks this in the source:

  *lwr++ = x
might look like this in the AST:

  block
    statement (assignment)
      expression
        operator (post-increment)
          operator (dereference)
            variable
      expression
        variable
 
If these two ASTs are to take the same form in the compilation pipeline, there needs to be some kind of pass that transforms one into the other.

As for why the second form is faster (or rather, why the compiler can generate faster code): There is likely an optimisation pass somewhere in llvm that recognises a pattern that this fits into, which allows it to generate branchless instructions. For instance, in the second form, there is a pattern of:

  operator (post-increment)
    operator (dereference)
that might be recognised by a pass. In the former form, the two operators are far apart in the tree, so a pass would have to "look further" to match them up. A single pass likely won't do this, either for (compiler) performance reasons, or for correctness reasons.

Finding which pass that is can be non-trivial, as it's more than a matter of enabling individual passes until one works. It might be that an earlier pass does some code reshaping that allows the relevant pass to work. My suggestion would be to dump the llvm ir at the end, and find the rough pattern that you're looking for, then re-run the compilation with `-mllvm -print-after-all` to see what the IR looks like after each pass, and then manually "look back" until you can't see the pattern any more.


  block
    statement (assignment)
      expression
        operator (post-increment)
          operator (dereference)
            variable
      expression
        variable
I don't think this could possibly be a valid AST for '*lwr++ = x' because the increment is not an operation on the dereferenced value, its an operation on the pointer. So in this case I don't see how it could help but be transformed into a form similar to the "beginner friendly" case.

Or perhaps I am wrong and it would generate an AST like you describe and rely on later passes to actually create a proper dependency graph. My mental model of how these kinds of postfix operators work always assumed it must very early on turn it into two separate statements. Thank you for the suggestions.


Oops, you're right - I got the operators the wrong way around! I forget the precise precedence of * and ++ in C sometimes. Assuming that it would be bracketed as *(lwr++), it should actually be:

  block
    statement (assignment)
      expression
        operator (dereference)
          operator (post-increment)
            variable
      expression
        variable


It required a little bit of messing with optimisation settings and library generation in Rust, but they emit very very similar x86-64 assembly:

https://godbolt.org/z/89W4srz4d


Nice, thank you for picking up after my laziness. Surely only a few bytes different in the binary, and much, much smaller of a delta than the source.


You can further reduce the difference by passing Expr by pointer in the C version. At that point I think the only difference in the assembly is the order in which the cases are handed.


Ah yeah, honestly both should probably be passed by pointer anyway. But that makes me wonder about the actual differences here and why... maybe something fun to dig into.


Passing by pointer (in C) reduced the difference a lot, but swapping the order of Add and Int in the Rust enum was enough to reduce the different to:

  cmp ecx, 1
  je .LBB0_3
vs

  cmp ecx, 2
  jne .LBB0_2
LBB0_3 and LBBO_2 were the same in both outputs (up to alpha renaming).

Oddly, both sources seemed to be quite sensitive to match switch and enum reordering, resulting in very different generated code. Possibly something to look into further.


My understanding is that churches were built next to yew trees, not yew trees planted next to churches.

Pre-Christian religions had many associations with yew trees (they live for a long time, give off mildly hallucinogenic gasses on hot days, discourage animals), and so built their holy sites around them. When Christianity came to Britain, churches were deliberately built on pagan holy sites to overrun the old religions, in the same way that early Christianity took over roman holy days (Saturnalia -> Christmas, Lemuria -> All Saint's Day). This led to churches being built next to sites with copious yew trees.


The Christmas/ Saturnalia link is a myth.



Yes, it is.

Look at your source — hardly authoritative! And the so-called evidence... Thin, entirely circumstantial, and in places actually wrong.

For example, Saturnalia went — at its longest - until the 23rd, not the 25th. Moreover, Christians likely got the 25th date based on religious calculation[1], not with reference to the one of the ancient festivals Victorian "historians" liked to speculate about.

The article attempts to link gifting verses to writing Christmas cards — a Christmas tradition not popularised until the 19th C and no older than the 17th.

Feasting and exchanging of gifts, much less the greeting "lo saturnalia" — literally the only other "evidence" presented here — need hardly be addressed, given how ubiquitous such things are with regard to festival, and how thin the apparent connection.

There is no positive evidence for this link, hence it not being taken seriously by any modern historian. It is no more than outdated speculation.

[1] https://poj.peeters-leuven.be/content.php?id=3007366&url=art...


> Look at your source — hardly authoritative!

Still better than the source you originally gave, which was non-existent, or the one you've given now, which is Catholic propaganda.

It might be true that Christmas was not a direct rip-off of Saturnalia per se (though we will probably never know for sure because the Church surely did not go out of its way to record this) but there can be no doubt that both Christmas and Saturnalia are solstice festivals, so at the very least they share a common motivational root. The exact provenance of the details doesn't really matter. What matters is that Jesus was almost certainly not born on December 25, and so whatever the motivation for that date was, Jesus's birth wasn't it.


I see — a blog written by an unknown librarian is a better source than an Oxford fellow writing for a journal with a hundred year pedigree. No further engagement necessary, I think.


The Daily Mail has a 130 year pedigree.

  Untruthful! My nephew Algernon! Impossible! He is an Oxonian!


When the journal in question is called "Studies in Liturgy", yeah, a blog post is better. At least the blog post isn't manifestly biased.


The fundamental issue with this is that many problems have a time/energy/financial threshold for success. Trying to tackle such a problem with incremental iterative solutions will consistently fail, as each individual iteration will fail.

This is most obvious when network effects are present (e.g. local immunisation efforts vs country-wide immunisation), but it's surprisingly common in other government-related areas like welfare, childcare, social security etc.

Edit: Another comment has reminded me that affordable public transport is the perfect example of this: Incrementally building out a public transport system will almost always fail, as the initial lines (be they buses, light rail, etc) will typically not be successful enough to justify the cost of building the line. If, instead, a system is built out universally and simultaneously, the utility (and thus income) of each line increases due to the interconnected nature of the network.


Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: