sparkie
Joined 14 years ago
Joined 14 years ago
* 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.
Both government and academia know how to navigate this well - by avoiding accountability.
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.Should probably use MeCab, which parses this correctly.
(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.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.
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
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.
> 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.
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).
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.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`.
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:
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