HNHacker News
TopNewBestAskShowJobs

ot

18,939 karma · joined September 28, 2010

Giuseppe Ottaviano

  - Twitter: @ot_y
  - LinkedIn: www.linkedin.com/in/giuseppe-ottaviano-497b7b3
submissionscomments
ot··on UTF-8 is a brilliant design
> It was so easy once we saw it that there was no reason to keep the placemat for notes, and we left it behind. Or maybe we did bring it back to the lab; I'm not sure. But it's gone now.

https://commandcenter.blogspot.com/2020/01/utf-8-turned-20-y...

ot··on %CPU utilization is a lie
It is a linear percentage of the amount of time the CPU is not idle. It is not linear in the amount of useful work, but that's not what "utilization" means.

The lie is the assumption that CPU time is linear in useful work, but that has nothing to do with the definition of utilization, it's just something that people sometimes naively believe.

> CPU utilization isn't a lie, % CPU utilization is

What do you mean by this? Utilization is, by definition, a ratio. % just determines that the scale is in [0, 100].

ot··on %CPU utilization is a lie
Utilization is not a lie, it is a measurement of a well-defined quantity, but people make assumptions to extrapolate capacity models from it, and that is where reality diverges from expectations.

Hyperthreading (SMT) and Turbo (clock scaling) are only a part of the variables causing non-linearity, there are a number of other resources that are shared across cores and "run out" as load increases, like memory bandwidth, interconnect capacity, processor caches. Some bottlenecks might come even from the software, like spinlocks, which have non-linear impact on utilization.

Furthermore, most CPU utilization metrics average over very long windows, from several seconds to a minute, but what really matters for the performance of a latency-sensitive server happens in the time-scale of tens to hundreds of milliseconds, and a multi-second average will not distinguish a bursty behavior from a smooth one. The latter has likely much more capacity to scale up.

Unfortunately, the suggested approach is not that accurate either, because it hinges on two inherently unstable concepts

> Benchmark how much work your server can do before having errors or unacceptable latency.

The measurement of this is extremely noisy, as you want to detect the point where the server starts becoming unstable. Even if you look at a very simple queueing theory model, the derivatives close to saturation explode, so any nondeterministic noise is extremely amplified.

> Report how much work your server is currently doing.

There is rarely a stable definition of "work". Is it RPS? Request cost can vary even throughout the day. Is it instructions? Same, the typical IPC can vary.

Ultimately, the confidence intervals you get from the load testing approach might be as large as what you can get from building an empirical model from utilization measurement, as long as you measure your utilization correctly.

ot··on Shared_ptr<T>: the (not always) atomic reference counted smart pointer (2019)
I'm not sure which comment you're responding to, because I'm not talking about shared_ptr, but about how atomic operations in general are implemented on x86.

I don't believe that shared_ptr uses seq-cst because I can just look at the source code, and I know that inc ref is relaxed and dec ref is acq-rel, as they should be.

However, none of this makes a difference on x86, where RMW atomic operations all lower to the same instructions (like LOCK ADD). Loads also do not care about memory order, and stores sometimes do, and that was what my comment was about.

ot··on Patrick Winston: How to Speak (2018) [video]
The joke is almost 5 minutes into the talk: he didn't start with one. His point is that in the first few minutes the audience is still warming up and many wouldn't pay attention to the joke.
ot··on Shared_ptr<T>: the (not always) atomic reference counted smart pointer (2019)
Not a L1/L2/... cache flush, but a store buffer flush, at least on x86. This is true for LOCK instructions. Loads/stores (again on x86) are always acquire/release, so they don't need additional fences if you don't need seq-cst. However, seq-cst atomics in C++ lower stores to LOCK XCHG, so you get a fence.
ot··on This is my brain on leeches
From Wikipedia (https://en.wikipedia.org/wiki/Hirudo_medicinalis)

> Because of the minuscule amounts of hirudin present in leeches, it is impractical to harvest the substance for widespread medical use. Hirudin (and related substances) are synthesized using recombinant techniques. Devices called "mechanical leeches" that dispense heparin and perform the same function as medicinal leeches have been developed, but they are not yet commercially available.

ot··on White House loyalty rating for companies
> Trump works transactionally

Why can't we just call this corruption? Is there any other, more charitable, interpretation of "transactional"?

ot··on How we made JSON.stringify more than twice as fast
The SWAR escaping algorithm [1] is very similar to the one I implemented in Folly JSON a few years ago [2]. The latter works on 8 byte words instead of 4 bytes, and it also returns the position of the first byte that needs escaping, so that the fast path does not add noticeable overhead on escape-heavy strings.

[1] https://source.chromium.org/chromium/_/chromium/v8/v8/+/5cbc...

[2] https://github.com/facebook/folly/commit/2f0cabfb48b8a8df84f...

ot··on SIMD within a register: How I doubled hash table lookup performance
Now it's O(0.5)
ot··on Log by time, not by count
Yes, but crucially, only 1/period, not on every single "should log?" call, which is what I was referring to above. The per-thread mutexes are uncontended virtually all the time.
ot··on Log by time, not by count
> Either the threads all do their separate “actually log”

But why? Often the purpose is just to log a "been here" signal with some additional details for diagnostics. You don't need to include an accumulation of everything that happened since the last log. All that you care about is that the log happens at most 1/period, say once per second.

If you do want to also log some data that accumulates everything that happened, you can accumulate the data in thread-local buffers, and in the "actually log" part collect all the buffers and log them. Since this only happens in the thread that "wins" the CAS, it is still very scalable. This is a very common technique.

If you throttle by count, you cannot avoid the contended atomic increment (you can with some sophistication and at the cost of some approximation).

ot··on Log by time, not by count
No, the timer is still global (that's why you need the compare-and-swap). But the threads only need to do reads most of the time, and reads do not cause contention. Writes do.

It looks something like this (pseudocode):

    static std::atomic<uint64_t> deadline{0};
    auto now = coarse_clock::now();
    auto curDeadline = deadline.load(std::memory_order_relaxed);
    if (now >= curDeadline &&
        deadline.compare_exchange_strong(curDeadline, now + period, std::memory_order_relaxed)) {
      // Actually log
    }
ot··on Log by time, not by count
There is an additional benefit to throttling by time, it is a lot easier to do it efficiently in multithreaded environments.

If you log by count, you need a global counter for that event (you could do thread-local, but then your logging volume would depend on the number of threads). If the code path is hot (which may be the case if you want to throttle your logs) multiple threads will contend on the increment, and that can be very expensive.

If you log by time, you just need a load and a clock read (on Linux, `CLOCK_MONOTONIC_COARSE` is a handful of ns and the resolution is enough for this purpose), and only need synchronization (a compare-and-swap) when the timer expires, so threads virtually never interfere with each other.

ot··on C++: Zero-cost static initialization
`NoDestructor` just ensures that the destructor is not called on the wrapped object, but you still need to manage the lifetime. If you look at the example, its recommended usage is with a function static. In other words, it's a utility to implement leaky Meyers' singletons.

std::launder is a bit weird here. Technically it should be used every time you use placement new but access the object by casting the pointer to its storage (which NoDestructor does). However, very little code actually uses it. For example, every implementation of std::optional should use it? But when you do, it actually prevents important compiler optimizations that make std::optional a zero-cost abstraction (or it did last time I looked into this).

ot··on C++: Zero-cost static initialization
Yes, thanks for the clarification, what I probably should have said is that the trick is basically syntactic sugar to declare a scoped global static variable, and as such it inherits all the problems of global static variables.
ot··on C++: Zero-cost static initialization
Not with any of the Clang versions I tried, but last time I checked it was a couple of years ago.
ot··on C++: Zero-cost static initialization
That's a nice trick, but contrary to function statics, it is susceptible to SIOF. This kind of optimization is useful only on extraordinarily hot paths, so I wouldn't generally recommend it.

> On ARM, such atomic load incurs a memory barrier---a fairly expensive operation.

Not quite, it is just a load-acquire, which is almost as cheap as a normal load. And on x86 there's no difference.

One thing where both GCC and Clang seem to be quite bad at is code layout: even in the example in the article, the slow path is largely inlined. It would be much better to have just a load, a compare, and a jump to the slow path in a cold section. In my experience, in some rare cases reimplementing the lazy initialization explicitly (especially when it's possible to use a sentinel value, thus doing a single load for both value and guard) did produce a noticeable win.

ot··on Bought myself an Ampere Altra system
I would guess to develop and test software that will ultimately run on a system with 64k page size.
ot··on Meta: Shut Down Your Invasive AI Discover Feed. Now
> While the company insists that “nothing is shared unless you choose to post it,” the app nonetheless nudges people to share—and overshare—whether they fully realize it or not.
ot··on The case of the UI thread that hung in a kernel call
On Linux you'd do this by sending a signal to the thread you want to analyze, and then the signal handler would take the stack trace and send it back to the watchdog.

The tricky part is ensuring that the signal handler code is async-signal-safe (which pretty much boils down to "ensure you're not acquiring any locks and be careful about reentrant code"), but at least that only has to be verified for a self-contained small function.

Is there anything similar to signals on Windows?

ot··on Tell HN: Announcing tomhow as a public moderator
Karma is stored in a 16 bit integer. It overflowed.
ot··on Performance of the Python 3.14 tail-call interpreter
Being more robust to fragile compiler optimizations is also a nontrivial benefit. An interpreter loop is an extremely specialized piece of code whose control flow is too important to be left to compiler heuristics.

If the desired call structure can be achieved in a portable way, that's a win IMO.

ot··on Questioning the Criteria for Evaluating Non-Cryptographic Hash Functions
The fact that they categorize FNV-1a as "Good all-rounder, decent speed and collision resistance" was an immediate red flag for me.

It does look like an article written more than 10 years ago.

ot··on The Ribbon Microphone
That is a ridiculously well made video, thanks for sharing!
ot··on Formal Methods: Just Good Engineering Practice? (2024)
Nothing wrong with reposts, it's just useful to link to previous discussions for context :)
ot··on Formal Methods: Just Good Engineering Practice? (2024)
Previous discussion (Jun 2024): https://news.ycombinator.com/item?id=40753989
ot··on Pat Gelsinger was wrong for Intel
Arrogance being one of the main criticisms in the article is a little ironic, coming from bcantrill.
ot··on Bit-twiddling optimizations in Zed's Rope
What that code does is a per-byte-pair popcount, which is not what the POPCNT instruction does (it computes the popcount for the whole word).

On processors with BMI2 the whole algorithm reduces to a PDEP as mentioned in another comment, but if you don't have that this is pretty much the best you can do (unless you use lookup tables but those have pros and cons).

ot··on Limitations of frame pointer unwinding
I broadly agree with the thesis of the post, which if I understand correctly is that frame pointers are a temporary compromise until the whole ecosystem gets its act together and manages to agree on some form of out-of-band tracking of frame pointers, and it seems that we'll eventually get there.

Some of the statements in the post seem odd to me though.

- 5% of system-wide cycles spent in function prologues/epilogues? That is wild, it can't be right.

- Is using the whole 8 bytes right for the estimate? Pushing the stack pointer is the first instruction in the prologue and it's literally 1 byte. Epilogue is symmetrical.

- Even if we're in the prologue, we know that we're in a leaf call, we can still resolve the instruction pointer to the function, and we can read the return address to find the parent, so what information is lost?

When it comes to future alternatives, while frame pointers have their own problems, I think that there are still a few open questions:

- Shadow stacks are cool but aren't they limited to a fixed number of entries? What if you have a deeper stack?

- Is the memory overhead of lookup tables for very large programs acceptable?

← PreviousPage 2 of 30Next →