Recent submissions

Comment · 12 hours ago
If you don't need insert/remove in middle, only at the end, there are more optimal data structures for Lists 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

Comment · 1 day ago
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.
Comment · 1 day ago
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.

Comment · 1 day ago
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.

Comment · 13 days ago
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.
Comment · 14 days ago
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.

Comment · 15 days ago
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)
Comment · 20 days ago

  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.
Comment · 20 days ago
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.

Comment · 20 days ago
Comment · 20 days ago
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.

Comment · 1 month ago
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.

Comment · 1 month ago
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).

Comment · 1 month ago
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.
Comment · 1 month ago
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`.

Live from the official Hacker News API

A modern reader for Hacker News · SvelteKit + Skeleton