News for hackers. Stuff that matters.

sparkie

Re:It's the Memory Latency, Stupid on Sunday November 01, 2026 @07:17AM (#50324921)
If you don't need insert/remove in middle, only at the end, there are more optimal data structures for list like RAOTS[1].

Btrees are O(log n) random access, O(log n) cons and use O(log n) excess space.

Unrolled linked lists are still O(n) random access because we ignore the constant factor of block size. They also use O(n) excess space for pointers, but have O(1) cons.

RAOTS are O(1) random access, O(1) amortized cons [O(√n) worse case], and use O(√n) excess space.

RAOTS is basically a tree of depth 1. The root is an "index block" of pointers to data blocks which grow in size proportional to the square root of the data length (similar to Hashed Array Trees, but doesn't require restructuring on each power of 2). The index block has length ~√n.

The square root is an approximation - the structure grows in "superblocks" where each superblock has `~√n` blocks of size `~√n` The pattern is: [1x1, 1x2, 2x2, 2x4, 4x4, 4x8, 8x8, 8x16, 16x16, 16x32, ...] Even indexed superblocks double the data block size, odd indexed superblocks double the number of blocks per superblock. It doesn't use an actual `sqrt()` call uses but some bit tricks via `clz`.

A simpler trick, which has no single name but have been known as "segmented lists", is where the index block points to blocks increasing in size geometrically essentially like a dynamic array but where we don't need to `realloc`. The index block is length 1+log₂n and each index i points to a block of length 2⁽ⁱ⁻¹⁾. These have O(1) random access, O(1) amortized cons [O(n) worst case] and waste O(n) space [with constant factor of 1/2]. The random access is very efficient as it's only two dereferences and a few bit tricks:

    index_block[bit_width(idx)][idx - bit_floor(idx)]
This is more cache friendly than the other approaches.

There's also Bagwell's VList[2] which is like a mashup of the "segmented list" and an unrolled list - the blocks grow geometrically in size, but still contain the pointer the the next block rather than them being in a separate index block.

[1]:https://cs.uwaterloo.ca/research/tr/1999/09/CS-99-09.pdf

[2]:https://dl.acm.org/doi/10.5555/1756972.1756975

Re:Serbia just elected the world's brainiest parliament on Saturday October 31, 2026 @10:10AM (#50316383)
Well, you can have certain differences of opinion as long as they're "acceptable" opinions. Academia ousts those who don't fit into its culture. Tenure is the only way you can express your real opinion without reprisal, though you may still be socially outcast for having the "wrong" opinion - and those who make it this far are predominantly products of that same culture - their opinions align with their peers on what is deemed acceptable. The anointed.
Re:Serbia just elected the world's brainiest parliament on Saturday October 31, 2026 @09:58AM (#50316261)
You can take what you see here and extrapolate. HN is overwhelmingly "intellectuals" - many academics, though not all. We can see:

* Everyone is quick to make judgement on something they don't know all the details of.

* There's not much agreement - mostly people disagreeing with each other.

* Everyone's opinion is important - they're smart so obviously, they're always right.

* Unrelated politics - my pet issue must be talked about.

* Anything is better - it's impossible for things to get worse.

* I disagree with this opinion so it must be a Russian bot.

Anyway, I wonder what will happen if you get together a bunch of intellectuals in government.

Re:Serbia just elected the world's brainiest parliament on Saturday October 31, 2026 @09:06AM (#50315839)
It's more the kind of deciding how to spend other people's money.

Both government and academia know how to navigate this well - by avoiding accountability.

Re:Types of types: Haskell's Kind system on Monday October 19, 2026 @01:43PM (#50155428)
I find it easier to think of them as transforms which turn "a function which accepts/returns plain values" into a "function which accepts and returns a type which embeds plain values".

    map     :: (a -> b)     -> (t a -> t b) -- Functor
    bind    :: (a -> t b)   -> (t a -> t b) -- Monad
    cobind  :: (t a -> b)   -> (t a -> t b) -- Comonad
    ap      :: t (a -> b)   -> (t a -> t b) -- Applicative
The resulting function type is the same in all these cases - the difference is the type of the input function. There are other similar functions which return a `(t a -> t b)` too.

The "ordered computation" isn't unique to monads, it applies to all of these when you chain them. For monads particularly though, you can't begin the continuation until the previous result has been generated - whereas with a comonad, you can begin the continuation right away and extract the value afterwards (inside the continuation).

Can also think of it like a producer/consumer or push/pull thing. Monads push values into some type `t`, and comonads pull values out of `t`.

The naming of `return` is perhaps unfortunate as it gives the impression you're returning a value (done to make do-notation look like imperative code) - but monads don't return values. `return` produces a computation.

`coreturn` is usually called `extract`, which is better naming - it extracts a value from a computation.

The order of arguments is also unfortunate because it makes the above pattern less obvious - they're usually written as:

    fmap :: f a -> (a -> b) -> f b
    bind :: m a -> (a -> m b) -> m b
So it's not obvious that `fmap` is transforming a function `(a -> b)` into one `(f a -> f b)`, etc.
Re:Furifont, a Japanese font that shows furigana on Sunday October 18, 2026 @01:14PM (#50143081)
It's not learning. The description says it does a maximal munch, which is obviously going to be wrong a lot of the time.

Should probably use MeCab, which parses this correctly.

Re:Yet another way to invoke functions without parentheses on Saturday October 17, 2026 @06:38AM (#50129814)
There's also function composition

   (h . g . f) x
Or for LtR, with Control.Category

   (f >>> g >>> h) x
Could combine with the forward pipe for

   x |> (f >>> g >>> h)

  typedef struct {
      double value;
      int    error;
  } maybe_double;
Is most efficient on x86-64 SYSV, because the ABI supports the `error` being returned in a GP register, and the `value` in a vector register. The struct is returned in `rax:xmm0` - the double is where it is meant to be.

When you use a union containing an int and a double, the double has to be moved to a GP register, and back to the vector register when read as a double (via movq), so there's a small hidden cost compared to the former approach. Your `Result_double` is returned in `rax:rdx`.

There's a trick you can use to avoid this - keep the `double` in the vector register and move the `int` to the vector register instead - so you only pay the cost to move between GP and vector registers when there's an error but not otherwise. To do this, make the union use `__m64` instead of `int` - the whole union is then in a vector register, and the `Result_double` struct is returned in `rax:xmm0`, as with `maybe_double` struct.

   typedef struct {
        _Bool ok;
        union {
            double value;
            __m64 error;
        };
    } Result_double;
Of course, this is x86-64 SYSV only. On AARCH64, the double will always be moved to a GP register in either example. Other conventions may also differ - MSVC in particular is poor because it converts to pass-by-pointer to a stack allocated structure for any struct greater than 64-bits.

On MSVC we could maybe get maximum efficiency by using NaN-boxing - store the error code in the signalling NaN space - eg, treat NaNs beginning 0x7FF1 as error codes.

   typedef union {
       int64_t _as_int;
       double _as_double;
   } Result_double;
   
   #define Result_double_value(n) \
       (Result_double){ ._as_double = (n) };
   #define Result_double_error(errcode) \ 
       (Result_double){ ._as_int = (0x7FF1000000000000ULL | (errcode)) }
   #define Result_double_is_error(x) \
       (((x)._as_int & 0x7FFF000000000000ULL) == 0x7FF1000000000000ULL)
   #define Result_double_get_error(x) \
       ((x)._as_int & 0x0000FFFFFFFFFFFFULL)
   #define Result_double_get_value(x) \
       ((x)._as_double)

   Result_double x = strtod·or·error(s, NULL)
   if (Result_double_is_error(x)) {
      handle(Result_double_get_error(x));
      return;
   }
   use(Result_double_get_value(x))
The `Result_double` then gets returned in `rax` and avoids touching the stack. Could probably improve this to return in `xmm0` instead.
Returning 2 values can be more performant than returning one and assigning to another as an "out parameter".

Eg:

    typedef struct {
        bool has_value;
        int value;
    } option_int_t;

    option_int_t foo() {
        if (somecond) return (option_int_t){ true, somevalue };
        else return (option_int_t){ false };
    }
  
    bool bar(int *out) {
        if (somecond) {
            *out = somevalue;
            return true;
        }
        return false;
    }
`foo` is slightly more efficient than `bar` on 64-bit SYSV, whose ABI supports 128-bit ("two eightbytes") returns in hardware registers. `bar` requires at least one dereference to assign the result.

On other compilers this isn't the case. MSVC only supports 64-bit returns and would use the stack for `option_int_t`. The caller allocates the space and an implicit pointer is passed in the first argument register (eg, `rcx`), and the same pointer is returned as the result.

Sum types in the set theoretic approach can be made with `(A & ¬B) | (¬A & B)`, or equivalently `(A | B) & (¬A | ¬B)`.

There's no mainstream language with negation types, but a few good research languages with them, eg MLstruct.

[MLstruct]:https://github.com/hkust-taco/mlstruct

If you do:

    typedef struct {
        int status;
        float value;
    } div_result_t;

    div_result_t divide(float a, float b) {
        if (b = 0.0) return (div_result_t){ E_DIVZERO };
        return (div_result_t){ 0, a / b };
    }
On SYSV: The `status` gets returned in x0/rax, and `value` gets returned in x1/xmm0.

On MSVC: The caller allocates `sizeof(div_result_t)` on the stack, passes the the pointer to that space as an implicit first argument to `divide` in register x0/rcx. The function returns the same pointer as its result in x0/rax.

The latter is almost equivalent to you passing a pointer manually - but the compiler does it for you. In the former case, it's actually more efficient to use the struct as we don't even need to touch the stack - all arguments and returns are in registers (for structs up to two "eightbytes" - ie, 16-bytes).

IMO, if it intended to use automatic storage duration, and the size of the struct is small enough, returning by value is preferable - it performs better on SYSV, and basically automates what you would do manually on MSVC. If it is intended for manual storage duration, then yes, use pass and return by pointer explicitly.

Re:C's Flexible Integer Sizes Were Not a Design Mistake on Sunday September 27, 2026 @07:15PM (#49871781)
That's an implementation choice. The standard makes no such guarantees.

> An integer may be converted to any pointer type. Except as previously specified, the result is implementation-defined, may not be correctly aligned, may not point to an entity of the referenced type, and can produce an indeterminate representation when stored into an object

The compiler may specify otherwise. GCC specifies that the pointer cast to integer back to pointer must point to the original object, else the behavior is undefined.

Re:C's Flexible Integer Sizes Were Not a Design Mistake on Sunday September 27, 2026 @06:51PM (#49871613)
Partial register stalls occurred when you assigned something to say `eax`, then read `ax`, or vice versa - if you wrote `ax` then read `eax`. If you wrote to `ax` then read `ax`, there was no stall. This was due to the register renaming implementation.

AFAIK, this was only an issue in some older CPUs and isn't a problem today.

32-bit `int` is still cheaper than 16-bits though, because 16-bit instructions require an operand size override prefix (0x66), or address size override prefix (0x67), or both. Technically, these aren't "16 bit prefixes" - if the machine was running in 16-bit protected mode, then you would need those prefixes to use 32-bit instructions and the non-prefixed ones would be 16-bit and thus cheaper, so `int` would be better as 16-bits in 16-bit protected mode - though this mode is essentially unused today, so for all intents and purposes the prefixes are used to issue 16-bit instructions and 16-bits is more expensive (in code size, i-cache usage, which may impact performance).

Re:C's Flexible Integer Sizes Were Not a Design Mistake on Sunday September 27, 2026 @05:56PM (#49871263)
If you want to know how bad it can be, only need to take a look at GCC's implementation of `stddef.h` for `size_t` and `ptrdiff_t` etc.

https://github.com/gcc-mirror/gcc/blob/master/gcc/ginclude/s...

There's a simpler way to define these types in newer versions of C which have `typeof`, we can take advantage of the fact that `sizeof()` always returns a `size_t`, `ptr - ptr` always returns a `ptrdiff_t`, and a literal L'c' has type `wchar_t`, etc.

    typedef typeof(sizeof(0)) size_t;
    typedef typeof(nullptr) nullptr_t;
    typedef typeof((void*)0-(void*)0) ptrdiff_t;
    typedef typeof(L'\0') wchar_t;
    typedef typeof(u8'\0') char8_t;
    typedef typeof(u'\0') char16_t;
    typedef typeof(U'\0') char32_t;
Don't need ifdef soup to test what the sizes of things are on different architectures - we just utilize the compiler's implementation of those types.
Re:C's Flexible Integer Sizes Were Not a Design Mistake on Sunday September 27, 2026 @05:40PM (#49871135)
It's safe to dereference in the same thread.

A `thread_local`'s actual virtual address is not merely what the pointer contains - it's an offset from some other virtual address stored in the FS or GS register (on x86-64), which is swapped when you change thread. Casting the pointer to `intptr_t` does not retain the segment base address - only the offset. The pointer is not the absolute address.

If you dereference in another thread, it's the same offset, but from a different base address.

There may be other things besides segment registers on other architectures that also make it unsafe. The C standard makes no guarantee that you can safely dereference a pointer cast from `intptr_t`.

Re:C's Flexible Integer Sizes Were Not a Design Mistake on Sunday September 27, 2026 @05:35PM (#49871091)
Maybe you are talking past grandparent, but I think they meant "it's safe to assume float and double are IEEE-754," which is not mandated by the C standard.
Re:C's Flexible Integer Sizes Were Not a Design Mistake on Sunday September 27, 2026 @05:25PM (#49871015)
`int` being 32-bits on amd64 was the correct decision, even in hindsight. If you are familiar with the ISA you will understand this. Existing 32-bit code just worked on the 64-bit chip, because the instruction encodings for 32-bit are unchanged. The 64-bit instructions are basically "opt-in", by placing a REX prefix on them - that makes them more expensive to encode and uses more instruction cache. Even today, compilers will emit 32-bit versions of instructions when the upper 32-bits are not needed, because it's cheaper.

When amd64 was released, x86 was almost ubiquitous on desktops and ran the majority of servers - most of the software used by the world could continue being used. If AMD had not gone through this effort to make it backward compatible, it's likely IA64 would've won and we wouldn't have this debate. Hard to understate the importance of not breaking things.

If you were designing a greenfield 64-bit ISA, then yes, it might make sense to have `int` be 64-bits, but it was definitely not, and still is not a mistake that it's 32-bits on amd64.

On RISC-V for example, it's questionable. The RV32 ecosystem is tiny and almost irrelevant - if they decided to break things for RV64 it wouldn't be a big problem - probably better to fix any problems early rather than hold baggage to run software that never existed - though it's much easier to port software to RV64 if `int` is still 32-bits.

Re:C's Flexible Integer Sizes Were Not a Design Mistake on Sunday September 27, 2026 @04:58PM (#49870758)
Yeah, in GCC for example, we can define exact width types without including `<stdint.h>`.

    typedef unsigned __attribute__((mode(HI))) uint16_t;
    typedef signed __attribute__((mode(SI))) int32_t;
Of course, the mode needs to be supported by the compiler for the target arch, but we don't need to include anything.
Re:C's Flexible Integer Sizes Were Not a Design Mistake on Sunday September 27, 2026 @03:57PM (#49870238)
Yeah, but my point is that two pointers can compare equal but point to different addresses, thus it's not necessarily a safe operation to dereference a pointer cast from `intptr_r`.
Re:C's Flexible Integer Sizes Were Not a Design Mistake on Sunday September 27, 2026 @07:48AM (#49865847)
I think backward compatibility was the main aim. Intel tried redesigning the architecture as 64-bit native (Itanium), but AMD done a better job at backward compatibility - and intel eventually adopted it as x86-64.

The amd64 design could run most 32-bit code with minimal changes. All the 32-bit instructions had the same encoding, besides push/pop which instead acted on 64-bits. The 64-bit instructions were basically opt-in, though a few opcodes (0x40..0x4F) had to be deprecated for the REX prefix.

Re:C's Flexible Integer Sizes Were Not a Design Mistake on Sunday September 27, 2026 @06:46AM (#49865483)
> int is basically the signed version of size_t aka a word sized data type. It's not meant to have a fixed size.

It isn't. `int` is at least as large as `short` and at least 16-bits. On modern systems `int` is still typically 32-bits whereas `size_t` is typically 64-bits. There's a `ssize_t` in POSIX for signed sizes.

> When people want the classic 4 byte data type they should choose long instead.

`long` is only at least 4 bytes, and at least as large as `int`. On MSVC (LLP64 data model) it's 4 bytes, but on SYSV (LP64 data model) it's 8 bytes. `long` should almost never be used if you actually want portable code today.

`int` is 32-bits and `long long` is 64-bits on both LP64 and LLP64. If you want portable code using the native integer types, these are the ones you should use, definitely not `long`.

Re:C's Flexible Integer Sizes Were Not a Design Mistake on Sunday September 27, 2026 @06:14AM (#49865268)
What is the size of a pointer though?

On Intel 286 we had a 16-bit machine word and 24-bit addresses. A pointer wasn't just two machine words concatenated - the upper 8 bits were stored somewhere else - a segment register.

On modern machines we don't (usually) need to consider this because we have a single linear virtual address space, though the size is architecture dependant - usually above 40 bits and below 64. Most common size is 48-bits, but also up to 57-bits with 5 level paging enabled.

Either way we round up to 64-bits to store the pointer as one integer. C optionally provides types `intptr_t` and `uintptr_t`, which are integers large enough to hold the value of a pointer. Converting a pointer to `intptr_t` and back to the pointer type results in a pointer that compares equal to the original.

However, there is no guarantee that a pointer converted to `intptr_t` and back to a pointer can be dereferenced! It works most of the time because of our linear address space and non-use of segmentation, but segmentation can still be used - the FS and GS segment registers are still available on x86_64 and are commonly used for thread local storage. If you take a `thread_local T*`, convert it to `intptr_t`, and then convert it back to a `thread_local T*` on another thread and attempt to dereference it, then despite the pointers comparing equal, they dereference to different virtual addresses.

Integers tied to the size of a pointer would have been misguided. Pointers are not integers! (They just happen to use an integer in their representation).

Another one, `size_t` is supposed to represent the maximum size of any object. However, that's also not well-defined. The maximum object size on the Intel 256 would have been 16-bits, because that is all you can fit in a single segment.

On a modern machine, a `size_t` should really be 48-bits (4LP) or 57-bits (5LP), because we can't have an object larger than our maximum virtual address size - but `size_t` is typically 64-bits.

PEGs aren't contained within the context-free languages, so it's not really intuitive that they're weaker, particularly as nobody has yet come up with a context-free language that a PEG cannot parse. They're capable of parsing things context-free grammars cannot.
Worth looking into nested words aka visibly pushdown languages.

They're a proper superset of regular languages and a proper subset of deterministic context-free languages, but they retain many of the nice properties of regular languages that DCFLs don't - they're closed under intersection, union, concatenation, Kleene Star and reversal.

They can parse more languages that Regular Expressions (non PCRE), but fewer than deterministic CFG subsets like LL/LR. They're expressive enough to parse languages which have a regular tree structure like S-expressions, JSON, XHTML.

Re:Zig: Pointer Stability for ArrayLists on Sunday August 30, 2026 @10:44PM (#49505035)
I made a mini example in C.

It's awkward to do get right because you need an indirect pointer whose address remains fixed, but points to another pointer which can change (and is volatile).

While it might be possible to make something like this lockless - it's much simpler to stick a mutex in the array header. When we access the array_segment we can take a lock to prevent some other thread reallocating mid-way through accessing.

There's probably a few improvements that could be made. In particular it doesn't handle use-after-free, so it's not thread safe w.r.t cleanup.

https://godbolt.org/z/rYzn5KGre

Re:Zig: Pointer Stability for ArrayLists on Sunday August 30, 2026 @07:37PM (#49503916)
I think parent was after base+offset+index rather than just base+index.

Examples would be eg, `string_view` or `ArraySegment`. They hold some offset relative to a base allocation, and when we index the string_view or ArraySegment we're indexing relative to that offset.

Re:Zig: Pointer Stability for ArrayLists on Sunday August 30, 2026 @07:31PM (#49503878)
That's Fat pointers, not Far pointers. A fat pointer is a pointer with some other associated data which is stored in the pointer itself - typically by widening the number of bits used to hold a pointer value. The addressable bits usually remain unchanged - the added bits contain the auxiliary data.

Segmentation isn't used. There's no separate registers to hold the bounds information in CHERI - the bounds are held in the pointer value, unlike for example, the now obsolete Intel MPX, which held bounds information in separate registers.

There's some similarity to segmentation because the CHERI pointer restricts which addresses can be accessed, but I wouldn't compare them to far pointers.

Most modern processors have a single linear virtual address space and don't use segmentation, and even where segment registers exist (eg, FS and GS on x86-64), they're only superficial "address spaces" - allocated sections of the process's linear virtual address space which could be accessed without segmentation registers if you knew the base address held in FS or GS.

Re:Zig: Pointer Stability for ArrayLists on Sunday August 30, 2026 @05:56PM (#49503266)
Far pointers are for accessing memory in different segments. They're basically obsolete now. They were necessary in older machines with limited sized pointers or address spaces.

GCC still supports `__seg_fs` and `__seg_gs`, which behave similar to `far` in the example on the wiki page, as the FS and GS segment registers are still valid in x86-64 and used for TLS. Clang uses attributes `address_space(257)` and `address_space(256)` for the same thing.

The `__based` pointer in MSVC exploits the addressing modes by pinning the base in eg: `[base+index*scale+displacement]`. It's unrelated to segmentation.

The poorest in society are the lowest CO2 emitters - they don't fly often, many don't own a car and use public transport - they can't afford meat in every meal - can't afford air con in summer or heating in winter.

Conversely, the wealthiest are frequent flyers, are chauffeured about in SUVs and rarely walk. They have everything they'll ever need and don't concern themselves about running the AC or heating.

They're preaching to the poor guy about what he needs to do to stop climate change.

Even if the poor guy wanted to care, he can't do much, but the people who claim to care could do a lot more, if they really did care.

The media circus tries to put the blame on the poor, while ignoring the emissions of the celebrities and treating them as royalty. Is it any wonder most people simply don't care?

If the media cared, they would be making villains of the celebrities and idols of the poor.

Re:Zig: Pointer Stability for ArrayLists on Sunday August 30, 2026 @05:11PM (#49502859)
The FS and GS segment selectors are still used in x86-64, typically for `thread_local` storage, but they can be repurposed.

`thread_local` is an example of a "relative pointer" though. Instructions to access the thread local are prefixed with `fs:` or `gs:`, and point relative to the address in the respective segment register.

Older »