2,318 karma · joined February 15, 2012
In the first `my_func`, there is the possiblity that `a` and `b` are equal if their struct layouts are equivalent (or one has a proper subset of the other's fields in the same order). To tell the compiler they don't overlap we would use `(strong_type1 *restrict a, strong_type2 *restrict b)`.
There's also the possibility that the pointers could point to the same address but be non-equal - eg if LAM/UAI/TBI are enabled, a simple pointer equality comparison is not sufficient because the high bits may not be equal. Or on platforms where memory access is always aligned, the low bits may be not equal. These bits are sometimes used to tag pointers with additional information.
This was my point. It may be `unsigned long` on his machine (or any that use LP64), but that isn't what `uint64_t` means. `uint64_t` means a type that is 64-bits, whereas `unsigned long` is simply a type that is larger than `unsigned int` and at least 32-bits, and `unsigned long long` is a type that is at least as large as `unsigned long` and is at least 64-bits.
I was not aware of compilers rejecting the equivalence of `long` and `long long` on LP64. GCC on Linux certainly doesn't. On windows it would be the case because it uses LLP64 where `long` is 32-bits and `long long` is 64-bits.
An intrinsic like `_addcarry_u64` should be using the `uint64_t` type, since its behavior depends on it being precisely 64-bits, which neither `long` nor `long long` guarantee. Intel's intrinsics spec defines it as using the type `unsigned __int64`, but since `__int64` is not a standard type, it has probably implemented as a typedef or `#define __int64 long long` by the compiler or `<immintrin.h>` he is using.
The Axum language had `domain` types, which could contain one or more `agent` and some state. Agents could have multiple functions and could share domain state, but not access state in other domains directly. The programming model was passing messages between agents over a typed `channel` using directional infix operators, which could also be used to build process pipelines. The channels could contain `schema` types and a state-machine like protocol spec for message ordering.
It didn't have "classes", but Axum files could live in the same projects as regular C# files and call into them. The C# compiler that came with it was modified to introduce an `isolated` keyword for classes, which prevented them from accessing `static` fields, which was key to ensuring state didn't escape the domain.
The software and most of the information was scrubbed from MS own website, but you can find an archived copy of the manual[1]. I still have a copy of the software installer somewhere but I doubt it would work on any recent Windows.
Sadly this project was axed before MS had embraced open source. It would've been nice if they had released the source when the decided to discontinue working on it.
[1]:https://web.archive.org/web/20110629202213/http://download.m...
If we were performing 128-bit arithmetic in parallel over many values, then a SIMD implementation may help, but without a SIMD equivalent of `addcarry`, there's a limit to how much it can help.
Something like this could potentially be added to AVX-512 for example by utilizing the `k` mask registers for the carries.
The best we have currently is `adcx` and `adox` which let us use two interleaved addcarry chains, where one utilizes the carry flag and the other utilizes the overflow flag, which improves ILP. These instructions are quite niche but are used in bigint libraries to improve performance.
The comment on the typedef points out that the signature of intrinsics uses `unsigned long long`, though he incorrectly states that `uint64_t` is `unsigned long` - which isn't true, as long is only guaranteed to be at least 32-bits and at least as large as `int`. In ILP64 and LLP64 for example, `long` is only 32-bits.
I don't think this really matters anyway. `long long` is 64-bits on pretty much everything that matters, and he is using architecture-specific intrinsics in the code so it is not going to be portable anyway.
If some future arch had 128-bit hardware integers and a data model where `long long` is 128-bits, we wouldn't need this code at all, as we would just use the hardware support for 128-bits.
But I agree that `uint64_t` is the correct type to use for the definition of `u128`, if we wanted to guarantee it occupies the same storage. The width-specific intrinsics should also use this type.
> I would be interested to see how all these operations fair against compiler-specific implementations
There's a godbolt link at the top of the article which has the comparison. The resulting assembly is basically equivalent to the built-in support.
They're typically implemented with arrays of 64-bit or 32-bit unsigned integers, but if 128-bits were available in hardware, we could get a performance boost. Any arbitrary precision integer library would benefit from 128-bit hardware integers.
Hmm, I found a solution and it was easier than expected. GCC has `__attribute__((designated_init))` we can stick on the struct which prevents positional initializers and requires the field names to be used (assuming -Werror). Since those names are poisoned, we won't be able to initialize except through functions defined in our library. We can similarly use a macro and #undef it.
Full encapsulation of a struct defined in a header:
#ifndef FOO_STRING_H
#define FOO_STRING_H
#include <stddef.h>
#include <stdlib.h>
#include <string.h>
#if defined __has_include
# if __has_include("config.h")
# include "config.h"
# endif
#endif
typedef size_t string_length_t;
#ifdef CONFIG_STRING_LENGTH_MAX
#define STRING_LENGTH_MAX CONFIG_STRING_LENGTH_MAX
#else
#define STRING_LENGTH_MAX (1 << 24)
#endif
typedef struct __attribute__((designated_init)) {
const string_length_t _internal_string_length;
const char *const _internal_string_chars;
} string_t;
#define STRING_CREATE(len, ptr) (string_t){ ._internal_string_length = (len), ._internal_string_chars = (ptr) }
#define STRING_LENGTH(s) (s._internal_string_length)
#define STRING_CHARS(s) (s._internal_string_chars)
#pragma GCC poison _internal_string_length _internal_string_chars
constexpr string_t error_string = STRING_CREATE(0, nullptr);
constexpr string_t empty_string = STRING_CREATE(0, "");
inline static string_t string_alloc_from_chars(const char *chars) {
if (__builtin_expect(chars == nullptr, false)) return error_string;
size_t len = strnlen(chars, STRING_LENGTH_MAX);
if (__builtin_expect(len == 0, false)) return empty_string;
if (__builtin_expect(len < STRING_LENGTH_MAX, true)) {
char *mem = malloc(len + 1);
strncpy(mem, chars, len);
mem[len] = '\0';
return STRING_CREATE(len, mem);
} else return error_string;
}
inline static const char *string_to_chars(string_t string) {
return STRING_CHARS(string);
}
inline static string_length_t string_length(string_t string) {
return STRING_LENGTH(string);
}
inline static void string_free(string_t s) {
free((char*)STRING_CHARS(s));
}
inline static bool string_is_valid(string_t string) {
return STRING_CHARS(string) != nullptr;
}
// ... other string function
#undef STRING_LENGTH
#undef STRING_CHARS
#undef STRING_CREATE
#endif /* FOO_STRING_H */
Aside from horrible pointer aliasing tricks, the only way to create a `string_t` is via `string_alloc_from_chars` or other functions defined in the library which return `string_t`. #include <stdio.h>
int main() {
string_t s = string_alloc_from_chars("Hello World!");
if (string_is_valid(s))
puts(string_to_chars(s));
string_free(s);
return 0;
}A technique I use is to leverage GCC's `poison` pragma to cause an error if attempting to access the struct's fields directly. I give the fields names that won't collide with anything, use macros to access them within the header and then `#undef` the macros at the end of the header.
Example - an immutable, pass-by-value string which couples the `char*` with the length of the string:
#ifndef FOO_STRING_H
#define FOO_STRING_H
#include <stddef.h>
#include <stdlib.h>
#include <string.h>
#include "config.h"
typedef size_t string_length_t;
#define STRING_LENGTH_MAX CONFIG_STRING_LENGTH_MAX
typedef struct {
string_length_t _internal_string_length;
char *_internal_string_chars;
} string_t;
#define STRING_LENGTH(s) (s._internal_string_length)
#define STRING_CHARS(s) (s._internal_string_chars)
#pragma GCC poison _internal_string_length _internal_string_chars
constexpr string_t error_string = { 0, nullptr };
constexpr string_t empty_string = { 0, "" };
inline static string_t string_alloc_from_chars(const char *chars) {
if (chars == nullptr) return error_string;
size_t len = strnlen(chars, STRING_LENGTH_MAX);
if (len == 0) return empty_string;
if (len < STRING_LENGTH_MAX) {
char *mem = malloc(len + 1);
strncpy(mem, chars, len);
mem[len] = '\0';
return (string_t){ len, mem };
} else return error_string;
}
inline static char * string_to_chars(string_t string) {
return STRING_CHARS(string);
}
inline static string_length_t string_length(string_t string) {
return STRING_LENGTH(string);
}
inline static void string_free(string_t s) {
free(STRING_CHARS(s));
}
inline static bool string_is_valid(string_t string) {
return STRING_CHARS(string) != nullptr
&& strnlen(STRING_CHARS(string), STRING_LENGTH_MAX) == STRING_LENGTH(string)
}
...
#undef STRING_LENGTH
#undef STRING_CHARS
#endif /* FOO_STRING_H */
It just wraps `<string.h>` functions in a way that is slightly less error prone to use, and adds zero cost. We can pass the string everywhere by value rather than needing an opaque pointer. It's equivalent on SYSV (64-bit) to passing them as two separate arguments: void foo(string_t str);
//vs
void foo(size_t length, char *chars);
These have the exact same calling convention: length passed in `rdi` and `chars` passed in `rsi`. (Or equivalently, `r0:r1` on other architectures).The main advantage is that we can also return by value without an "out parameter".
string_t bar();
//vs
size_t bar(char **out_chars);
These DO NOT have the same calling convention. The latter is less efficient because it needs to dereference a pointer to return the out parameter. The former just returns length in `rax` and chars in `rdx` (`r0:r1`).So returning a fat pointer is actually more efficient than returning a size and passing an out parameter on SYSV! (Though only marginally because in the latter case the pointer will be in cache).
Perhaps it's unfair to say "zero-cost" - it's slightly less than zero - cheaper than the conventional idiom of using an out parameter.
But it only works if the struct is <= 16-bytes and contains only INTEGER types. Any larger and the whole struct gets put on the stack for both arguments and returns. In that case it's probably better to use an opaque pointer.
That aside, when we define the struct in the header we can also `inline` most functions, so that avoids unnecessary branching overhead that we might have when using opaque pointers.
`#pragma GCC poison` is not portable, but it will be ignored wherever it isn't supported, so this won't prevent the code being compiled for other platforms - it just won't get the benefits we get from GCC & SYSV.
The biggest downside to this approach is we can't prevent the library user from using a struct initializer and creating an invalid structure (eg, length and actual string length not matching). It would be nice if there were some similar to trick to prevent using compound initializers with the type, then we could have full encapsulation without resorting to opaque pointers.
---
There are some variations of Chunk/Bucket arrays which offer different characteristics.
HAT (Hashed Array Trees[1]) are close to what is presented - the index block is always a power of 2 and points to data blocks of the same length - ie, it increases as: 1x1, 2x2, 4x4, 8x8, 16x16, ...
The downside is each time the length reaches a new power of 4 it requires the data to be shuffled about, which may cause latency spikes.
RAOTS (Resizeable Arrays in Optimal Time and Space[2]) is a variation where the index block block is approximately the square root of the length. It increases by adding a new block and resizing only the index block. The old blocks remain part of the structure and don't need reshuffling like HAT, which avoids the latency spikes, but there are more individual allocator calls. However, blocks are always a power of 2 which makes them arena-friendly.
Both of these have O(1) element access and waste only O(√n) space, which makes them quite optimal for uses where memory may be a constraint.
---
In the double-and-copy style array, one trick is we can avoid storing `capacity` and compute it on demand - determine the MSB of the length (using `lzcnt` or `bsr`), and then determine the next power of 2 sufficient to hold that length. In C23, this is `stdc_bit_ceil` (<stdbit.h>).
In the "Xal" structure (which has several other names), we can use `stdc_first_leading_one` to determine the index in the index block, then zero out the leading one bit to determine the index in the bucket.
This structure would be more efficient if processors provided a `log2mask` instruction which computes both the MSB and the remainder after masking the MSB in one instruction. (Similar to how `divmod` instructions work where the quotient and remainder are both computed in one instruction).
---
[1]: https://en.wikipedia.org/wiki/Hashed_array_tree
[2]:https://cs.uwaterloo.ca/research/tr/1999/09/CS-99-09.pdf
Then it's clearly only half a solution.
The example I gave above should work fine in any language with first-class closures.
#include <stdlib.h>
#include <string.h>
#include <stddef.h>
typedef int (*comp)(const void *untyped_left, const void *untyped_right);
thread_local int in_reverse = 0;
__attribute__((noinline))
int compare_impl(const void *untyped_left, const void *untyped_right, int in_reverse) {
const int* left = untyped_left;
const int* right = untyped_right;
return (in_reverse) ? *right - *left : *left - *right;
}
comp make_sort(int direction) {
in_reverse = direction;
int compare(const void *untyped_left, const void *untyped_right) {
return compare_impl(untyped_left, untyped_right, in_reverse);
}
return compare;
}
int main(int argc, char* argv[]) {
int list[] = { 2, 11, 32, 49, 57, 20, 110, 203 };
comp normal_sort = make_sort(0);
comp reverse_sort = make_sort(1);
qsort(list, (sizeof(list)/sizeof(*list)), sizeof(*list), normal_sort);
return list[0];
}
Because we create `reverse_sort` between creating `normal_sort` and calling it, we end up with a reverse sort despite clearly asking for a normal sort.Perhaps interesting for those who aren't up to date, the recent APX extension allows 3-operand versions of most of the ALU instructions with a new data destination, so we don't need to use temporary registers - making them more RISC-like.
The downside is they're EVEX encoded, which adds a 4-byte prefix to the instruction. It's still cheaper to use `lea` for an addition, but now we will be able to do things like
or rax, rdx, rcx
https://www.intel.com/content/www/us/en/developer/articles/t...In `op reg1, reg2`, the two registers are encoded as 3 bits each the ModRM byte which follows the opcode. Obviously, we can't fit 3 registers in the ModRM byte because it's only 8-bits.
In `op reg1, [reg2 + reg3]`, reg1 is encoded in the ModRM byte. The 3 bits that were previously used for reg2 are instead `0b100`, which indicates a SIB byte follows the ModRM byte. The SIB (Scale-Index-Base) byte uses 3 bits each for reg2 and reg3 as the base and index registers.
In any other instruction, the SIB byte is used for addressing, so syntax of `lea` is consistent with the way it is encoded.
Encoding details of ModRM/SIB are in Volume2, Section 2.1.5 of the ISA manual: https://www.intel.com/content/www/us/en/developer/articles/t...
The reason `xor eax,eax` is preferred to `xor rax,rax` is due to how the instructions are encoded - it saves one byte which in turn reduces instruction cache usage.
When using 64-bit operations, a REX prefix is required on the instruction (byte 0x40..0x4F), which serves two purposes - the MSB of the low nybble (W) being set (ie, REX prefixes 0x48..0x4f) indicates a 64-bit operation, and the low 3 bits of low nybble allow using registers r8-r15 by providing an extra bit for the ModRM register field and the base and index fields in the SIB byte, as only 3-bits (8-registers) are provided by x86.
A recent addition, APX, adds an additional 16 registers (r16-r31), which need 2 additional bits. There's a REX2 prefix for this (0xD5 ...), which is a two byte prefix to the instruction. REX2 replaces the REX prefix when accessing r16-r31, still contains the W bit, but it also includes an `M0` bit, which says which of the two main opcode maps to use, which replaces the 0x0F prefix, so it has no additional cost over the REX prefix when accessing the second opcode map.
The seL4 kernel uses an Object-Capability system (ironically which have also been around since the '70s) in place of Access Control Lists used in POSIX systems. The idea behind capabilities is "don't separate designation from authority" - the capability is both a designator of a resource and the authority to use it - there are no separate ACLs. Separation of designation from authority leads to confused deputies - where one process with lower privileges can trick a higher-privileged process to use the higher privileges on its behalf, which very often leads to privilege escalation vulnerabilities (ie, RCE as root).
Each process has a "cspace" (Capability Space), which holds the capabilities the process may use. When the process performs a system call, it references a capability slot from a set of tables in its own cspace where the capability is held, and the Kernel verifies that the capability is valid in order to perform the syscall. The capabilities can be revoked at any time, and importantly, they cannot be forged - only delegated.
Additionally, seL4 is a microkernel design which is intended to have a small attack surface. It provides only a small number of system calls like `Send`, `Recv`, `Yield`, and combinations of, such as `Call`, `Reply`, `ReplyRecv`. It's a generic interface where an IPC buffer is passed along with a set of delegated capabilities, but there's no specific calls such as `SYS_write` or `SYS_mmap` - these are built on top of the `Send`/`Recv` syscalls and their services are provided by other user-space daemon processes.
One of the best developments of seL4 is the "Mixed-criticality system" support, which provides capabilities for processing time - to enable some processes to have real-time support without becoming CPU hogs.
seL4 can also be seen as a hypervisor which can virtualize other kernels, and part of the Lions effort is to be able to utilize Linux drivers using isolated Linux guests.
To learn more, the seL4 manual is a good place to start: https://sel4.systems/Info/Docs/seL4-manual-latest.pdf. There's some videos on Youtube by Gernot Heiser discussing the MCS support.
#define BYTE_VALUES \
X(0x00) X(0x01) X(0x02) X(0x03) X(0x04) X(0x05) X(0x06) X(0x07) X(0x08) X(0x09) X(0x0A) X(0x0B) X(0x0C) X(0x0D) X(0x0E) X(0x0F) \
X(0x10) X(0x11) X(0x12) X(0x13) X(0x14) X(0x15) X(0x16) X(0x17) X(0x18) X(0x19) X(0x1A) X(0x1B) X(0x1C) X(0x1D) X(0x1E) X(0x1F) \
X(0x20) X(0x21) X(0x22) X(0x23) X(0x24) X(0x25) X(0x26) X(0x27) X(0x28) X(0x29) X(0x2A) X(0x2B) X(0x2C) X(0x2D) X(0x2E) X(0x2F) \
X(0x30) X(0x31) X(0x32) X(0x33) X(0x34) X(0x35) X(0x36) X(0x37) X(0x38) X(0x39) X(0x3A) X(0x3B) X(0x3C) X(0x3D) X(0x3E) X(0x3F) \
X(0x40) X(0x41) X(0x42) X(0x43) X(0x44) X(0x45) X(0x46) X(0x47) X(0x48) X(0x49) X(0x4A) X(0x4B) X(0x4C) X(0x4D) X(0x4E) X(0x4F) \
X(0x50) X(0x51) X(0x52) X(0x53) X(0x54) X(0x55) X(0x56) X(0x57) X(0x58) X(0x59) X(0x5A) X(0x5B) X(0x5C) X(0x5D) X(0x5E) X(0x5F) \
X(0x60) X(0x61) X(0x62) X(0x63) X(0x64) X(0x65) X(0x66) X(0x67) X(0x68) X(0x69) X(0x6A) X(0x6B) X(0x6C) X(0x6D) X(0x6E) X(0x6F) \
X(0x70) X(0x71) X(0x72) X(0x73) X(0x74) X(0x75) X(0x76) X(0x77) X(0x78) X(0x79) X(0x7A) X(0x7B) X(0x7C) X(0x7D) X(0x7E) X(0x7F) \
X(0x80) X(0x81) X(0x82) X(0x83) X(0x84) X(0x85) X(0x86) X(0x87) X(0x88) X(0x89) X(0x8A) X(0x8B) X(0x8C) X(0x8D) X(0x8E) X(0x8F) \
X(0x90) X(0x91) X(0x92) X(0x93) X(0x94) X(0x95) X(0x96) X(0x97) X(0x98) X(0x99) X(0x9A) X(0x9B) X(0x9C) X(0x9D) X(0x9E) X(0x9F) \
X(0xA0) X(0xA1) X(0xA2) X(0xA3) X(0xA4) X(0xA5) X(0xA6) X(0xA7) X(0xA8) X(0xA9) X(0xAA) X(0xAB) X(0xAC) X(0xAD) X(0xAE) X(0xAF) \
X(0xB0) X(0xB1) X(0xB2) X(0xB3) X(0xB4) X(0xB5) X(0xB6) X(0xB7) X(0xB8) X(0xB9) X(0xBA) X(0xBB) X(0xBC) X(0xBD) X(0xBE) X(0xBF) \
X(0xC0) X(0xC1) X(0xC2) X(0xC3) X(0xC4) X(0xC5) X(0xC6) X(0xC7) X(0xC8) X(0xC9) X(0xCA) X(0xCB) X(0xCC) X(0xCD) X(0xCE) X(0xCF) \
X(0xD0) X(0xD1) X(0xD2) X(0xD3) X(0xD4) X(0xD5) X(0xD6) X(0xD7) X(0xD8) X(0xD9) X(0xDA) X(0xDB) X(0xDC) X(0xDD) X(0xDE) X(0xDF) \
X(0xE0) X(0xE1) X(0xE2) X(0xE3) X(0xE4) X(0xE5) X(0xE6) X(0xE7) X(0xE8) X(0xE9) X(0xEA) X(0xEB) X(0xEC) X(0xED) X(0xEE) X(0xEF) \
X(0xF0) X(0xF1) X(0xF2) X(0xF3) X(0xF4) X(0xF5) X(0xF6) X(0xF7) X(0xF8) X(0xF9) X(0xFA) X(0xFB) X(0xFC) X(0xFD) X(0xFE) X(0xFF)
void resolve(Transaction *t) {
void* jt[] = {
#define X(n) [n] = &&abandon##n,
BYTE_VALUES
#undef X
[0] = &send,
};
static uint8_t branch = 0;
bool csend = should_send(t);
branch = csend ? 0 : branch; // cmov or SETcc
goto * jt[branch];
#define X(n) \
abandon##n: \
abandon(t); \
branch = random() % 256; \
return;
BYTE_VALUES
#undef X
send:
if (csend) send(t);
else abandon(t);
return;
}
Assuming no inherent bias in the low byte produced by `random`, there's only a ~1/255 chance that an abandon branch will correctly predict, though this is also true for the send branch. The conditional branch in send though should only mispredict 1/256 times (when random returns 0).If we're sending significantly more often than 1/256 calls to resolve, it may be possible to train the BTP to prefer the send branch, as it will correctly predict this branch more often than the others which are chosen randomly - though this would depend on how the branch target predictor is implemented in the processor.
People get cancelled not for saying that it is genetic, but for questioning whether it may be. Of course, we will never know if we're not allowed to ask. Cancel culture is anti-science.
Watson may have been racist, but questioning whether there is a relationship between genetics and intelligence by itself is not racism.
If everyone stops asking questions and asks the LLM instead, there is no new training data for future LLMs to learn from. They will stagnate, or consume their own slop, and regress.
> This is indeed a weak-point in the contract approach: People can't be bound by an contract they never knew about nor agreed-to.
"Prominent notice" is important in the terms of use approach. Many terms of use claims have been dismissed because there was failure to give prominent notice - however, there have been successes, such as Hubbert vs Dell[1], where an appeals court reversed the trial court's decision and ruled in Dell's favour on the basis that they had given prominent notice of terms.
[1]:https://caselaw.findlaw.com/court/il-court-of-appeals/124479...
There are other potential legal avenues besides contract law, such as Unjust Enrichment[2], which, according to Wiki is analysed as:
1. Was the defendant enriched?
2. Was the enrichment at the expense of the claimant?
3. Was the enrichment unjust?
4. Does the defendant have a defense?
5. What remedies are available to the claimant?
[2]:https://en.wikipedia.org/wiki/Restitution_and_unjust_enrichm...Since AI companies are likely to be enriched (by a large amount), and it could be at the expense of a claimant if the AI (re)produces the claimants work or closely related work based on it (or in the case of a class action suit, potentially many works), the AI companies which violate a terms of use would have to argue in their favour for #3 and #4 - is the enrichment unjust and do they have a defense.
There are certainly arguments for AI trained on copyrighted works being unjust. Including a terms of use which specifically prevents this would be in the favor of the claimant. An AI company would have to defend their decision to ignore such terms and claim that doing so is not unjust.
Arguably, if the AI is sufficiently "intelligent" enough, it should be able to make sense of such terms and be used to filter such works from training data (unless specifically prompted to ignore). If the AI companies are not filtering the data they aggregate then there's a potential argument for negligence.
There's some efforts being made, such as the IETF aipref[3], which could standardize the way training data is collected/filtered by AI companies. Creative Commons have a related effort called "CC signals". These could also be helpful in a future claim if the AI companies ignore those preferences.
[3]:https://datatracker.ietf.org/wg/aipref/about/
So to me it seems having a clause in your license and/or terms of use which prevents use in AI training, providing prominent notice of such terms, and indicating AI preferences in the protocols, is going to be better than not having such clauses - because if you don't have the clause then the defendant can simply claim in their defense that "There was nothing in the terms which said we can't".
It's up to us to take a proactive approach to protecting our works from AI, even if there is not yet any solid legal basis or case law, so I applaud OP's efforts even if the license doesn't have carry any weight. If we don't attempt to protect our works from use AI training then the AI companies will win at everyone else's expense.
Creators should decide how their works are used, and we need to normalize it being the case that they decide whether or not use in AI training data should be permitted.
The high bits let us tag more types, and can be used in conjunction with low bits tagging. Eg, we might use the low bits for GC marking.
[1]:https://web.archive.org/web/20170705085007/ftp://ftp.cs.indi...