More from Evan Jones - Software Engineer | Computer Scientist
You can't safely use the C setenv() or unsetenv() functions in a program that uses threads. Those functions modify global state, and can cause other threads calling getenv() to crash. This also causes crashes in other languages that use those C standard library functions, such as Go's os.Setenv (Go issue) and Rust's std::env::set_var() (Rust issue). I ran into this in a Go program, because Go's built-in DNS resolver can call C's getaddrinfo(), which uses environment variables. This cost me 2 days to track down and file the Go bug. Sadly, this problem has been known for decades. For example, an article from January 2017 said: "None of this is new, but we do re-discover it roughly every five years. See you in 2022." This was only one year off! (She wrote an update in October 2023 after I emailed her about my Go bug.) This is a flaw in the POSIX standard, which extends the C Standard to allow modifying environment varibles. The most infuriating part is that many people who could influence the standard or maintain the C libraries don't see this as a problem. The argument is that the specification clearly documents that setenv() cannot be used with threads. Therefore, if someone does this, the crashes are their fault. We should apparently read every function's specification carefully, not use software written by others, and not use threads. These are unrealistic assumptions in modern software. I think we should instead strive to create APIs that are hard to screw up, and evolve as the ecosystem changes. The C language and standard library continue to play an important role at the base of most software. We either need to figure out how to improve it, or we need to figure out how to abandon it. Why is setenv() not thread-safe? The biggest problem is that getenv() returns a char*, with no need for applications to free it later. One thread could be using this pointer when another thread changes the same environment variable using setenv() or unsetenv(). The getenv() function is perfect if environment variables never change. For example, for accessing a process's initial table of environment variables (see the System V ABI: AMD64 Section 3.4.1). It turns out the C Standard only includes getenv(), so according to C, that is exactly how this should work. However, most implementations also follow the POSIX standard (e.g. POSIX.1-2017), which extends C to include functions that modify the environment. This means the current getenv() API is problematic. Even worse, putenv() adds a char* to the set of environment variables. It is explicitly required that if the application modifies the memory after putenv() returns, it modifies the environment variables. This means applications can modify the value passed to putenv() at any time, without any synchronization. FreeBSD used to implement putenv() by copying the value, but it changed it with FreeBSD 7 in 2008, which suggests some programs really do depend on modifying the environment in this fashion (see FreeBSD putenv man page). As a final problem, environ is a NULL-terminated array of pointers (char**) that an application can read and assign to (see definition in POSIX.1-2017). This is how applications can iterate over all environment variables. Accesses to this array are not thread-safe. However, in my experience many fewer applications use this than getenv() and setenv(). However, this does cause some libraries to not maintain the set of environment variables in a thread-safe way, since they directly update this table. Environment variable implementations Implementations need to choose what do do when an application overwrites an existing variable. I looked at glibc, musl, Solaris/Illumos, and FreeBSD/Apple's C standard libraries, and they make the following choices: Never free environment variables (glibc, Solaris/Illumos): Calling setenv() repeatedly is effectively a memory leak. However, once a value is returned from getenv(), it is immutable and can be used by threads safely. Free the environment variables (musl, FreeBSD/Apple): Using the pointer returned by getenv() after another thread calls setenv() can crash. A second problem is ensuring the set of environment variables is updated in a thread-safe fashion. This is what causes crashes in glibc. glibc uses an array to hold pointers to the "NAME=value" strings. It holds a lock in setenv() when changing this array, but not in getenv(). If a thread calling setenv() needs to resize the array of pointers, it copies the values to a new array and frees the previous one. This can cause other threads executing getenv() to crash, since they are now iterating deallocated memory. This is particularly annoying since glibc already leaks environment variables, and holds a lock in setenv(). All it needs to do is hold the lock inside getenv(), and it would no longer crash. This would make getenv() slightly slower. However, getenv() already uses a linear search of the array, so performance does not appear to be a concern. More sophisticated implementations are possible if this is a problem, such as Solaris/Illumos's lock-free implementation. Why do programs use environment variables? Environment variables useful for configuring shared libraries or language runtimes that are included in other programs. This allows users to change the configuration, without program authors needing to explicitly pass the configuration in. One alternative is command line flags, which requires programs to parse them and pass them in to the libraries. Another alternative are configuration files, which then need some other way to disable or configure, to be able to test new configurations. Environment variables are a simple solution. AS a result, many libraries call getenv() (see a partial list below). Since many libraries are configured through environment variables, a program may need to change these variables to configure the libraries it uses. This is common at application startup. This causes programs to need to call setenv(). Given this issue, it seems like libraries should also provide a way to explicitly configure any settings, and avoid using environment variables. We should fix this problem, and we can In my opinion, it is rediculous that this has been a known problem for so long. It has wasted thousands of hours of people's time, either debugging the problems, or debating what to do about it. We know how to fix the problem. First, we can make a thread-safe implementation, like Illumos/Solaris. This has some limitations: it leaks memory in setenv(), and is still unsafe if a program uses putenv() or the environ variable. However, this is an improvement over the current Linux and Apple implementations. The second solution is to add new APIs to get one and get all environment variables that are thread-safe by design, like Microsoft's getenv_s() (see below for the controversy around C11's "Annex K"). My preferred solution would be to do both. This would reduce the chances of hitting this problem for existing programs and libraries, and also provide a path to avoid the problems entirely for new code or languages like Go and Rust. My rough idea would be the following: Add a function to copy one single environment variable to a user-specified buffer, similar to getenv_s(). Add a thread-safe API to iterate over all environment variables, or to copy all variables out. Mark getenv() as deprecated, recommending the new thread-safe getenv() function instead. Mark putenv() as deprecated, recommending setenv() instead. Mark environ as deprecated, recommending environment variable functions instead. Update the implementation of environment varibles to be thread-safe. This requires leaking memory if getenv() is used on a variable, but we can detect if the old functions are used, and only leak memory in that case. This means programs written in other languages will avoid these problems as soon as their runtimes are updated. Update the C and POSIX standards to require the above changes. This would be progress. The getenv_s / C Standard Annex K controversy Microsoft provides getenv_s(), which copies the environment variable into a caller-provided buffer. This is easy to make thread-safe by holding a read lock while copying the variable. After the function returns, future changes to the environment have no effect. This is included in the C11 Standard as Annex K "Bounds Checking Interfaces". The C standard Annexes are optional features. This Annex includes new functions intended to make it harder to make mistakes with buffers that are the wrong size. The first draft of this extension was published in 2003. This is when Microsoft was focusing on "Trustworthy Computing" after a January 2002 memo from Bill Gates. Basically, Windows wasn't designed to be connected to the Internet, and now that it was, people were finding many security problems. Lots of them were caused by buffer handling mistakes. Microsoft developed new versions of a number of problematic functions, and added checks to the Visual C++ compiler to warn about using the old ones. They then attempted to standardize these functions. My understanding is the people responsible for the Unix POSIX standards did not like the design of these functions, so they refused to implement them. For more details, see Field Experience With Annex K published in September 2015, Stack Overflow: Why didn't glibc implement _s functions? updated March 2023, and Rich Felker of musl on both technical and social reasons for not implementing Annex K from February 2019. I haven't looked at the rest of the functions, but having spent way too long looking at getenv(), the general idea of getenv_s() seems like a good idea to me. Standardizing this would help avoid this problem. Incomplete list of common environment variables This is a list of some uses of environment variables from fairly widely used libraries and services. This shows that environment variables are pretty widely used. Cloud Provider Credentials and Services AWS's SDKs for credentials (e.g. AWS_ACCESS_KEY_ID) Google Cloud Application Default Credentials (e.g. GOOGLE_APPLICATION_CREDENTIALS) Microsoft Azure Default Azure Credential (e.g. AZURE_CLIENT_ID) AWS's Lambda serverless product: sets a large number of variables like AWS_REGION, AWS_LAMBDA_FUNCTION_NAME, and credentials like AWS_SECRET_ACCESS_KEY Google Cloud Run serverless product: configuration like PORT, K_SERVICE, K_REVISION Kubernetes service discovery: Defines variables SERVICE_NAME_HOST and SERVICE_NAME_PORT. Third-party C/C++ Libraries OpenTelemetry: Metrics and tracing. Many environment variables like OTEL_SERVICE_NAME and OTEL_RESOURCE_ATTRIBUTES. OpenSSL: many configurable variables like HTTPS_PROXY, OPENSSL_CONF, OPENSSL_ENGINES. BoringSSL: Google's fork of OpenSSL used in Chrome and others. It reads SSLKEYLOGFILE just like OpenSSL for logging TLS keys for debugging. Libcurl: proxies, SSL/TLS configuration and debugging like HTTPS_PROXY, CURL_SSL_BACKEND, CURL_DEBUG. Libpq Postgres client library: connection parameters including credentials like PGHOSTADDR, PGDATABASE, and PGPASSWORD. Rust Standard Library std::thread RUST_MIN_STACK: Calls std::env::var() on the first call to spawn() a new thread. It is cached in a static atomic variable and never read again. See implementation in thread::min_stack(). std::backtrace RUST_LIB_BACKTRACE: Calls std::env::var() on the first call to capture a backtrace. It is cached in a static atomic variable and never read again. See implementation in Backtrace::enabled().
This is a reminder that random load balancing is unevenly distributed. If we distribute a set of items randomly across a set of servers (e.g. by hashing, or by randomly selecting a server), the average number of items on each server is num_items / num_servers. It is easy to assume this means each server has close to the same number of items. However, since we are selecting servers at random, they will have different numbers of items, and the imbalance can be important. For load balancing, a reasonable model is that each server has fixed capacity (e.g. it can serve 3000 requests/second, or store 100 items, etc.). We need to divide the total workload over the servers, so that each server stays below its capacity. This means the number of servers is determined by the most loaded server, not the average. This is a classic balls in bins problem that has been well studied, and there are some interesting theoretical results. However, I wanted some specific numbers, so I wrote a small simulation. The summary is that the imbalance depends on the expected number of items per server (that is, num_items / num_servers). This means workload is more balanced with fewer servers, or with more items. This means that dividing a set of items over more servers makes the distribution more unfair, which is a reason we can get worse than linear scaling of a distributed system. Let's make this more concrete with an example. Let's assume we have a workload of 1000 items, and each server can hold a maximum of 100 items. If we place the exact same number of items on each server, we only need 10 servers, and each of them is completely busy. However, if we place the items randomly, then the median (p50) number of items is 100 items. This means half the servers will have more than 100 items, and will be overloaded. If we want less than a 1% chance of an overloaded server, we need to look at the 99th percentile (p99) server load. We need to use at least 13 servers, which has a p99 load of 97 items. For 14 servers, the average is 77 items, so our servers are on average 23% idle. This shows how the imbalance leads to wasted capacity. This is a bit of an extreme example, because the number of items is small. Let's assume we can make the items 10× smaller, say by dividing them into pieces. Our workload now consists of 10k items, and each server has the capacity to hold 1000 (1k) items. Our perfectly balanced workload still needs 10 servers. With random load balancing, to have a less than 1 in 1000 chance of exceeding our capacity, we only need 11 servers, which has a p99 load of 98 items and a p999 of 100 items. With 11 servers, the average number of items is 910 or 91%, so our servers are only 9% idle. This shows how splitting work into smaller pieces improves load balancing. Another way to look at this is to think about a scaling scenario. Let's go back to our workload of 1000 items, where each server can handle 100 items, and we have 13 servers to ensure we have less than a 1% chance of an overloaded server. Now let's assume the amount of work per item doubles, for example because the service has become more popular, so each item has become larger. Now, each server can hold a maximum of 50 items. If we have perfectly linear scaling, we can double the number of servers from 13 to 26 to handle this workload. However, 26 servers has a p99 of 53 items, so we again have a more than 1% chance of overload. We need to use 28 servers which has a p99 of 50 items. This means we doubled the workload, but had to increase the number of servers from 13 to 28, which is 2.15×. This is sub-linear scaling. As a way to visualize the imbalance, the chart below shows the p99 to average ratio, which is a measure of how imbalanced the system is. If everything is perfectly balanced, the value is 1.0. A value of 2.0 means 1% of servers will have double the number of items of the average server. This shows that the imbalance increases with the number of servers, and increases with fewer items. Power of Two Random Choices Another way to improve load balancing is to have smarter placement. Perfect placement can be hard, but it is often possible to use the "power of two random choices" technique: select two servers at random, and place the item on the least loaded of the two. This makes the distribution much more balanced. For 1000 items and 100 items/server, 11 servers has a p999 of 93 items, so much less than 0.1% chance of overload, compared to needing 14 servers with random load balancing. For the scaling scenario where each server can only handle 50 items, we only need 21 servers to have a p999 of 50 items, compared to 28 servers with random load balancing. The downside of the two choices technique is that each request is now more expensive, since it must query two servers instead of one. However, in many cases where the "item not found" requests are much less expensive than the "item found" requests, this can still be a substantial improvement. For another look at how this improves load balancing, with a nice simulation that includes information delays, see Marc Brooker's blog post. Raw simulation output I will share the code for this simulation later. simulating placing items on servers with random selection iterations=10000 (number of times num_items are placed on num_servers) measures the fraction of items on each server (server_items/num_items) and reports the percentile of all servers in the run P99_AVG_RATIO = p99 / average; approximately the worst server compared to average num_items=1000: num_servers=3 p50=0.33300 p95=0.35800 p99=0.36800 p999=0.37900 AVG=0.33333; P99_AVG_RATIO=1.10400; ITEMS_PER_NODE=333.3 num_servers=5 p50=0.20000 p95=0.22100 p99=0.23000 p999=0.24000 AVG=0.20000; P99_AVG_RATIO=1.15000; ITEMS_PER_NODE=200.0 num_servers=10 p50=0.10000 p95=0.11600 p99=0.12300 p999=0.13100 AVG=0.10000; P99_AVG_RATIO=1.23000; ITEMS_PER_NODE=100.0 num_servers=11 p50=0.09100 p95=0.10600 p99=0.11300 p999=0.12000 AVG=0.09091; P99_AVG_RATIO=1.24300; ITEMS_PER_NODE=90.9 num_servers=12 p50=0.08300 p95=0.09800 p99=0.10400 p999=0.11200 AVG=0.08333; P99_AVG_RATIO=1.24800; ITEMS_PER_NODE=83.3 num_servers=13 p50=0.07700 p95=0.09100 p99=0.09700 p999=0.10400 AVG=0.07692; P99_AVG_RATIO=1.26100; ITEMS_PER_NODE=76.9 num_servers=14 p50=0.07100 p95=0.08500 p99=0.09100 p999=0.09800 AVG=0.07143; P99_AVG_RATIO=1.27400; ITEMS_PER_NODE=71.4 num_servers=25 p50=0.04000 p95=0.05000 p99=0.05500 p999=0.06000 AVG=0.04000; P99_AVG_RATIO=1.37500; ITEMS_PER_NODE=40.0 num_servers=50 p50=0.02000 p95=0.02800 p99=0.03100 p999=0.03500 AVG=0.02000; P99_AVG_RATIO=1.55000; ITEMS_PER_NODE=20.0 num_servers=100 p50=0.01000 p95=0.01500 p99=0.01800 p999=0.02100 AVG=0.01000; P99_AVG_RATIO=1.80000; ITEMS_PER_NODE=10.0 num_servers=1000 p50=0.00100 p95=0.00300 p99=0.00400 p999=0.00500 AVG=0.00100; P99_AVG_RATIO=4.00000; ITEMS_PER_NODE=1.0 num_items=2000: num_servers=3 p50=0.33350 p95=0.35050 p99=0.35850 p999=0.36550 AVG=0.33333; P99_AVG_RATIO=1.07550; ITEMS_PER_NODE=666.7 num_servers=5 p50=0.20000 p95=0.21500 p99=0.22150 p999=0.22850 AVG=0.20000; P99_AVG_RATIO=1.10750; ITEMS_PER_NODE=400.0 num_servers=10 p50=0.10000 p95=0.11100 p99=0.11600 p999=0.12150 AVG=0.10000; P99_AVG_RATIO=1.16000; ITEMS_PER_NODE=200.0 num_servers=11 p50=0.09100 p95=0.10150 p99=0.10650 p999=0.11150 AVG=0.09091; P99_AVG_RATIO=1.17150; ITEMS_PER_NODE=181.8 num_servers=12 p50=0.08350 p95=0.09350 p99=0.09800 p999=0.10300 AVG=0.08333; P99_AVG_RATIO=1.17600; ITEMS_PER_NODE=166.7 num_servers=13 p50=0.07700 p95=0.08700 p99=0.09100 p999=0.09600 AVG=0.07692; P99_AVG_RATIO=1.18300; ITEMS_PER_NODE=153.8 num_servers=14 p50=0.07150 p95=0.08100 p99=0.08500 p999=0.09000 AVG=0.07143; P99_AVG_RATIO=1.19000; ITEMS_PER_NODE=142.9 num_servers=25 p50=0.04000 p95=0.04750 p99=0.05050 p999=0.05450 AVG=0.04000; P99_AVG_RATIO=1.26250; ITEMS_PER_NODE=80.0 num_servers=50 p50=0.02000 p95=0.02550 p99=0.02750 p999=0.03050 AVG=0.02000; P99_AVG_RATIO=1.37500; ITEMS_PER_NODE=40.0 num_servers=100 p50=0.01000 p95=0.01400 p99=0.01550 p999=0.01750 AVG=0.01000; P99_AVG_RATIO=1.55000; ITEMS_PER_NODE=20.0 num_servers=1000 p50=0.00100 p95=0.00250 p99=0.00300 p999=0.00400 AVG=0.00100; P99_AVG_RATIO=3.00000; ITEMS_PER_NODE=2.0 num_items=5000: num_servers=3 p50=0.33340 p95=0.34440 p99=0.34920 p999=0.35400 AVG=0.33333; P99_AVG_RATIO=1.04760; ITEMS_PER_NODE=1666.7 num_servers=5 p50=0.20000 p95=0.20920 p99=0.21320 p999=0.21740 AVG=0.20000; P99_AVG_RATIO=1.06600; ITEMS_PER_NODE=1000.0 num_servers=10 p50=0.10000 p95=0.10700 p99=0.11000 p999=0.11320 AVG=0.10000; P99_AVG_RATIO=1.10000; ITEMS_PER_NODE=500.0 num_servers=11 p50=0.09080 p95=0.09760 p99=0.10040 p999=0.10380 AVG=0.09091; P99_AVG_RATIO=1.10440; ITEMS_PER_NODE=454.5 num_servers=12 p50=0.08340 p95=0.08980 p99=0.09260 p999=0.09580 AVG=0.08333; P99_AVG_RATIO=1.11120; ITEMS_PER_NODE=416.7 num_servers=13 p50=0.07680 p95=0.08320 p99=0.08580 p999=0.08900 AVG=0.07692; P99_AVG_RATIO=1.11540; ITEMS_PER_NODE=384.6 num_servers=14 p50=0.07140 p95=0.07740 p99=0.08000 p999=0.08300 AVG=0.07143; P99_AVG_RATIO=1.12000; ITEMS_PER_NODE=357.1 num_servers=25 p50=0.04000 p95=0.04460 p99=0.04660 p999=0.04880 AVG=0.04000; P99_AVG_RATIO=1.16500; ITEMS_PER_NODE=200.0 num_servers=50 p50=0.02000 p95=0.02340 p99=0.02480 p999=0.02640 AVG=0.02000; P99_AVG_RATIO=1.24000; ITEMS_PER_NODE=100.0 num_servers=100 p50=0.01000 p95=0.01240 p99=0.01340 p999=0.01460 AVG=0.01000; P99_AVG_RATIO=1.34000; ITEMS_PER_NODE=50.0 num_servers=1000 p50=0.00100 p95=0.00180 p99=0.00220 p999=0.00260 AVG=0.00100; P99_AVG_RATIO=2.20000; ITEMS_PER_NODE=5.0 num_items=10000: num_servers=3 p50=0.33330 p95=0.34110 p99=0.34430 p999=0.34820 AVG=0.33333; P99_AVG_RATIO=1.03290; ITEMS_PER_NODE=3333.3 num_servers=5 p50=0.20000 p95=0.20670 p99=0.20950 p999=0.21260 AVG=0.20000; P99_AVG_RATIO=1.04750; ITEMS_PER_NODE=2000.0 num_servers=10 p50=0.10000 p95=0.10500 p99=0.10700 p999=0.10940 AVG=0.10000; P99_AVG_RATIO=1.07000; ITEMS_PER_NODE=1000.0 num_servers=11 p50=0.09090 p95=0.09570 p99=0.09770 p999=0.09990 AVG=0.09091; P99_AVG_RATIO=1.07470; ITEMS_PER_NODE=909.1 num_servers=12 p50=0.08330 p95=0.08790 p99=0.08980 p999=0.09210 AVG=0.08333; P99_AVG_RATIO=1.07760; ITEMS_PER_NODE=833.3 num_servers=13 p50=0.07690 p95=0.08130 p99=0.08320 p999=0.08530 AVG=0.07692; P99_AVG_RATIO=1.08160; ITEMS_PER_NODE=769.2 num_servers=14 p50=0.07140 p95=0.07570 p99=0.07740 p999=0.07950 AVG=0.07143; P99_AVG_RATIO=1.08360; ITEMS_PER_NODE=714.3 num_servers=25 p50=0.04000 p95=0.04330 p99=0.04460 p999=0.04620 AVG=0.04000; P99_AVG_RATIO=1.11500; ITEMS_PER_NODE=400.0 num_servers=50 p50=0.02000 p95=0.02230 p99=0.02330 p999=0.02440 AVG=0.02000; P99_AVG_RATIO=1.16500; ITEMS_PER_NODE=200.0 num_servers=100 p50=0.01000 p95=0.01170 p99=0.01240 p999=0.01320 AVG=0.01000; P99_AVG_RATIO=1.24000; ITEMS_PER_NODE=100.0 num_servers=1000 p50=0.00100 p95=0.00150 p99=0.00180 p999=0.00210 AVG=0.00100; P99_AVG_RATIO=1.80000; ITEMS_PER_NODE=10.0 num_items=100000: num_servers=3 p50=0.33333 p95=0.33579 p99=0.33681 p999=0.33797 AVG=0.33333; P99_AVG_RATIO=1.01043; ITEMS_PER_NODE=33333.3 num_servers=5 p50=0.20000 p95=0.20207 p99=0.20294 p999=0.20393 AVG=0.20000; P99_AVG_RATIO=1.01470; ITEMS_PER_NODE=20000.0 num_servers=10 p50=0.10000 p95=0.10157 p99=0.10222 p999=0.10298 AVG=0.10000; P99_AVG_RATIO=1.02220; ITEMS_PER_NODE=10000.0 num_servers=11 p50=0.09091 p95=0.09241 p99=0.09304 p999=0.09379 AVG=0.09091; P99_AVG_RATIO=1.02344; ITEMS_PER_NODE=9090.9 num_servers=12 p50=0.08334 p95=0.08477 p99=0.08537 p999=0.08602 AVG=0.08333; P99_AVG_RATIO=1.02444; ITEMS_PER_NODE=8333.3 num_servers=13 p50=0.07692 p95=0.07831 p99=0.07888 p999=0.07954 AVG=0.07692; P99_AVG_RATIO=1.02544; ITEMS_PER_NODE=7692.3 num_servers=14 p50=0.07143 p95=0.07277 p99=0.07332 p999=0.07396 AVG=0.07143; P99_AVG_RATIO=1.02648; ITEMS_PER_NODE=7142.9 num_servers=25 p50=0.04000 p95=0.04102 p99=0.04145 p999=0.04193 AVG=0.04000; P99_AVG_RATIO=1.03625; ITEMS_PER_NODE=4000.0 num_servers=50 p50=0.02000 p95=0.02073 p99=0.02103 p999=0.02138 AVG=0.02000; P99_AVG_RATIO=1.05150; ITEMS_PER_NODE=2000.0 num_servers=100 p50=0.01000 p95=0.01052 p99=0.01074 p999=0.01099 AVG=0.01000; P99_AVG_RATIO=1.07400; ITEMS_PER_NODE=1000.0 num_servers=1000 p50=0.00100 p95=0.00117 p99=0.00124 p999=0.00132 AVG=0.00100; P99_AVG_RATIO=1.24000; ITEMS_PER_NODE=100.0 power of two choices num_items=1000: num_servers=3 p50=0.33300 p95=0.33400 p99=0.33500 p999=0.33600 AVG=0.33333; P99_AVG_RATIO=1.00500; ITEMS_PER_NODE=333.3 num_servers=5 p50=0.20000 p95=0.20100 p99=0.20200 p999=0.20300 AVG=0.20000; P99_AVG_RATIO=1.01000; ITEMS_PER_NODE=200.0 num_servers=10 p50=0.10000 p95=0.10100 p99=0.10200 p999=0.10200 AVG=0.10000; P99_AVG_RATIO=1.02000; ITEMS_PER_NODE=100.0 num_servers=11 p50=0.09100 p95=0.09200 p99=0.09300 p999=0.09300 AVG=0.09091; P99_AVG_RATIO=1.02300; ITEMS_PER_NODE=90.9 num_servers=12 p50=0.08300 p95=0.08500 p99=0.08500 p999=0.08600 AVG=0.08333; P99_AVG_RATIO=1.02000; ITEMS_PER_NODE=83.3 num_servers=13 p50=0.07700 p95=0.07800 p99=0.07900 p999=0.07900 AVG=0.07692; P99_AVG_RATIO=1.02700; ITEMS_PER_NODE=76.9 num_servers=14 p50=0.07200 p95=0.07300 p99=0.07300 p999=0.07400 AVG=0.07143; P99_AVG_RATIO=1.02200; ITEMS_PER_NODE=71.4 num_servers=25 p50=0.04000 p95=0.04100 p99=0.04200 p999=0.04200 AVG=0.04000; P99_AVG_RATIO=1.05000; ITEMS_PER_NODE=40.0 num_servers=50 p50=0.02000 p95=0.02100 p99=0.02200 p999=0.02200 AVG=0.02000; P99_AVG_RATIO=1.10000; ITEMS_PER_NODE=20.0 num_servers=100 p50=0.01000 p95=0.01100 p99=0.01200 p999=0.01200 AVG=0.01000; P99_AVG_RATIO=1.20000; ITEMS_PER_NODE=10.0 num_servers=1000 p50=0.00100 p95=0.00200 p99=0.00200 p999=0.00300 AVG=0.00100; P99_AVG_RATIO=2.00000; ITEMS_PER_NODE=1.0 power of two choices num_items=2000: num_servers=3 p50=0.33350 p95=0.33400 p99=0.33400 p999=0.33450 AVG=0.33333; P99_AVG_RATIO=1.00200; ITEMS_PER_NODE=666.7 num_servers=5 p50=0.20000 p95=0.20050 p99=0.20100 p999=0.20150 AVG=0.20000; P99_AVG_RATIO=1.00500; ITEMS_PER_NODE=400.0 num_servers=10 p50=0.10000 p95=0.10050 p99=0.10100 p999=0.10100 AVG=0.10000; P99_AVG_RATIO=1.01000; ITEMS_PER_NODE=200.0 num_servers=11 p50=0.09100 p95=0.09150 p99=0.09200 p999=0.09200 AVG=0.09091; P99_AVG_RATIO=1.01200; ITEMS_PER_NODE=181.8 num_servers=12 p50=0.08350 p95=0.08400 p99=0.08400 p999=0.08450 AVG=0.08333; P99_AVG_RATIO=1.00800; ITEMS_PER_NODE=166.7 num_servers=13 p50=0.07700 p95=0.07750 p99=0.07800 p999=0.07800 AVG=0.07692; P99_AVG_RATIO=1.01400; ITEMS_PER_NODE=153.8 num_servers=14 p50=0.07150 p95=0.07200 p99=0.07250 p999=0.07250 AVG=0.07143; P99_AVG_RATIO=1.01500; ITEMS_PER_NODE=142.9 num_servers=25 p50=0.04000 p95=0.04050 p99=0.04100 p999=0.04100 AVG=0.04000; P99_AVG_RATIO=1.02500; ITEMS_PER_NODE=80.0 num_servers=50 p50=0.02000 p95=0.02050 p99=0.02100 p999=0.02100 AVG=0.02000; P99_AVG_RATIO=1.05000; ITEMS_PER_NODE=40.0 num_servers=100 p50=0.01000 p95=0.01050 p99=0.01100 p999=0.01100 AVG=0.01000; P99_AVG_RATIO=1.10000; ITEMS_PER_NODE=20.0 num_servers=1000 p50=0.00100 p95=0.00150 p99=0.00200 p999=0.00200 AVG=0.00100; P99_AVG_RATIO=2.00000; ITEMS_PER_NODE=2.0 power of two choices num_items=5000: num_servers=3 p50=0.33340 p95=0.33360 p99=0.33360 p999=0.33380 AVG=0.33333; P99_AVG_RATIO=1.00080; ITEMS_PER_NODE=1666.7 num_servers=5 p50=0.20000 p95=0.20020 p99=0.20040 p999=0.20060 AVG=0.20000; P99_AVG_RATIO=1.00200; ITEMS_PER_NODE=1000.0 num_servers=10 p50=0.10000 p95=0.10020 p99=0.10040 p999=0.10040 AVG=0.10000; P99_AVG_RATIO=1.00400; ITEMS_PER_NODE=500.0 num_servers=11 p50=0.09100 p95=0.09120 p99=0.09120 p999=0.09140 AVG=0.09091; P99_AVG_RATIO=1.00320; ITEMS_PER_NODE=454.5 num_servers=12 p50=0.08340 p95=0.08360 p99=0.08360 p999=0.08380 AVG=0.08333; P99_AVG_RATIO=1.00320; ITEMS_PER_NODE=416.7 num_servers=13 p50=0.07700 p95=0.07720 p99=0.07720 p999=0.07740 AVG=0.07692; P99_AVG_RATIO=1.00360; ITEMS_PER_NODE=384.6 num_servers=14 p50=0.07140 p95=0.07160 p99=0.07180 p999=0.07180 AVG=0.07143; P99_AVG_RATIO=1.00520; ITEMS_PER_NODE=357.1 num_servers=25 p50=0.04000 p95=0.04020 p99=0.04040 p999=0.04040 AVG=0.04000; P99_AVG_RATIO=1.01000; ITEMS_PER_NODE=200.0 num_servers=50 p50=0.02000 p95=0.02020 p99=0.02040 p999=0.02040 AVG=0.02000; P99_AVG_RATIO=1.02000; ITEMS_PER_NODE=100.0 num_servers=100 p50=0.01000 p95=0.01020 p99=0.01040 p999=0.01040 AVG=0.01000; P99_AVG_RATIO=1.04000; ITEMS_PER_NODE=50.0 num_servers=1000 p50=0.00100 p95=0.00120 p99=0.00140 p999=0.00140 AVG=0.00100; P99_AVG_RATIO=1.40000; ITEMS_PER_NODE=5.0 power of two choices num_items=10000: num_servers=3 p50=0.33330 p95=0.33340 p99=0.33350 p999=0.33360 AVG=0.33333; P99_AVG_RATIO=1.00050; ITEMS_PER_NODE=3333.3 num_servers=5 p50=0.20000 p95=0.20010 p99=0.20020 p999=0.20030 AVG=0.20000; P99_AVG_RATIO=1.00100; ITEMS_PER_NODE=2000.0 num_servers=10 p50=0.10000 p95=0.10010 p99=0.10020 p999=0.10020 AVG=0.10000; P99_AVG_RATIO=1.00200; ITEMS_PER_NODE=1000.0 num_servers=11 p50=0.09090 p95=0.09100 p99=0.09110 p999=0.09110 AVG=0.09091; P99_AVG_RATIO=1.00210; ITEMS_PER_NODE=909.1 num_servers=12 p50=0.08330 p95=0.08350 p99=0.08350 p999=0.08360 AVG=0.08333; P99_AVG_RATIO=1.00200; ITEMS_PER_NODE=833.3 num_servers=13 p50=0.07690 p95=0.07700 p99=0.07710 p999=0.07720 AVG=0.07692; P99_AVG_RATIO=1.00230; ITEMS_PER_NODE=769.2 num_servers=14 p50=0.07140 p95=0.07160 p99=0.07160 p999=0.07170 AVG=0.07143; P99_AVG_RATIO=1.00240; ITEMS_PER_NODE=714.3 num_servers=25 p50=0.04000 p95=0.04010 p99=0.04020 p999=0.04020 AVG=0.04000; P99_AVG_RATIO=1.00500; ITEMS_PER_NODE=400.0 num_servers=50 p50=0.02000 p95=0.02010 p99=0.02020 p999=0.02020 AVG=0.02000; P99_AVG_RATIO=1.01000; ITEMS_PER_NODE=200.0 num_servers=100 p50=0.01000 p95=0.01010 p99=0.01020 p999=0.01020 AVG=0.01000; P99_AVG_RATIO=1.02000; ITEMS_PER_NODE=100.0 num_servers=1000 p50=0.00100 p95=0.00110 p99=0.00120 p999=0.00120 AVG=0.00100; P99_AVG_RATIO=1.20000; ITEMS_PER_NODE=10.0 power of two choices num_items=100000: num_servers=3 p50=0.33333 p95=0.33334 p99=0.33335 p999=0.33336 AVG=0.33333; P99_AVG_RATIO=1.00005; ITEMS_PER_NODE=33333.3 num_servers=5 p50=0.20000 p95=0.20001 p99=0.20002 p999=0.20003 AVG=0.20000; P99_AVG_RATIO=1.00010; ITEMS_PER_NODE=20000.0 num_servers=10 p50=0.10000 p95=0.10001 p99=0.10002 p999=0.10002 AVG=0.10000; P99_AVG_RATIO=1.00020; ITEMS_PER_NODE=10000.0 num_servers=11 p50=0.09091 p95=0.09092 p99=0.09093 p999=0.09093 AVG=0.09091; P99_AVG_RATIO=1.00023; ITEMS_PER_NODE=9090.9 num_servers=12 p50=0.08333 p95=0.08335 p99=0.08335 p999=0.08336 AVG=0.08333; P99_AVG_RATIO=1.00020; ITEMS_PER_NODE=8333.3 num_servers=13 p50=0.07692 p95=0.07694 p99=0.07694 p999=0.07695 AVG=0.07692; P99_AVG_RATIO=1.00022; ITEMS_PER_NODE=7692.3 num_servers=14 p50=0.07143 p95=0.07144 p99=0.07145 p999=0.07145 AVG=0.07143; P99_AVG_RATIO=1.00030; ITEMS_PER_NODE=7142.9 num_servers=25 p50=0.04000 p95=0.04001 p99=0.04002 p999=0.04002 AVG=0.04000; P99_AVG_RATIO=1.00050; ITEMS_PER_NODE=4000.0 num_servers=50 p50=0.02000 p95=0.02001 p99=0.02002 p999=0.02002 AVG=0.02000; P99_AVG_RATIO=1.00100; ITEMS_PER_NODE=2000.0 num_servers=100 p50=0.01000 p95=0.01001 p99=0.01002 p999=0.01002 AVG=0.01000; P99_AVG_RATIO=1.00200; ITEMS_PER_NODE=1000.0 num_servers=1000 p50=0.00100 p95=0.00101 p99=0.00102 p999=0.00102 AVG=0.00100; P99_AVG_RATIO=1.02000; ITEMS_PER_NODE=100.0
This is a post for myself, because I wasted a lot of time understanding this bug, and I want to be able to remember it in the future. I expect close to zero others to be interested. The C standard library function isspace() returns a non-zero value (true) for the six "standard" ASCII white-space characters ('\t', '\n', '\v', '\f', '\r', ' '), and any locale-specific characters. By default, a program starts in the "C" locale, which will only return true for the six ASCII white-space characters. However, if the program changes locales, it can return true for other values. As a result, unless you really understand locales, you should use your own version of this function, or ICU4C's u_isspace() function. An implementation of isspace() for ASCII is one line: /* Returns true for the 6 ASCII white-space characters: \t \n \v \f \r ' '. */ int isspace_ascii(int c) { return c == '\t' || c == '\n' || c == '\v' || c == '\f' || c == '\r' || c == ' '; } I ran into this because On Mac OS X, Postgres switches to the system's default locale, which is something that uses UTF-8 (e.g. en_US.UTF-8, fr_CA.UTF-8, etc). In this case, isspace() returns true for Unicode white-space values, which includes 0x85 = NEL = Next Line, and 0xA0 = NBSP = No-Break Space. This caused a bug in parsing Postgres Hstore values that use Unicode. I have attempted to submit a patch to fix this (mailing list post, commitfest entry). For a program to demonstrate the behaviour on different systems, see isspace_locale on Github.
Nearly all programs are written to access virtual memory addresses, which the CPU must translate to physical addresses. These translations are usually fast because the mappings are cached in the CPU's Translation Lookaside Buffer (TLB). Unfortunately, virtual memory on x86 has used a 4 kiB page size since the 386 was released in 1985, when computers had a bit less memory than they do today. Also unfortunately, TLBs are pretty small because they need to be fast. For example, AMD's Zen 4 Microarchitecture, which first shipped in September 2022, has a first level data TLB with 72 entries, and a second level TLB with 3072 entries. This means when an application's working set is larger than approximately 4 kiB × 3072 = 12 MiB, some memory accesses will require page table lookups, multiplying the number of memory accesses required. This is a brand-new CPU, with one of the biggest TLBs on the market, so most systems will be worse. Using larger virtual memory page sizes (aka huge pages) can reduce page mapping overhead substantially. Since RAM is so much larger than it was in 1985, a larger page size seems like obviously a good idea to me. In 2021, Google published a paper about making their malloc implementation (TCMalloc) huge page aware (called Temeraire). They report this improved average requests-per-second throughput across their fleet by 7%, by increasing the amount of memory that is backed by huge pages. This made me curious about the "best case" performance benefits. I wrote a small program that allocates 4 GiB, then randomly reads uint64 values from it. On my Intel 11th generation Core i5-1135G7 (Tiger Lake) from 2020, using 2 MiB huge pages is 2.9× faster. I also tried 1 GiB pages, which is 3.1× faster than 4 kiB pages, but only 8% faster than 2 MiB pages. My conclusion: Using madvise() to get the kernel to use huge pages seems like a relatively easy performance win for applications that use a large amount of RAM. Unfortunately, using larger pages is not without its disadvantages. Notably, when the Linux kernel's transparent huge page implementation was first introduced, it was enabled by default, which caused many performance problems. See the section below for more details. Today's default to use huge pages only for applications that opt-in (aka madvise) should improve this. The kernel's policies for managing huge pages have also changed since then, and are hopefully better now. At the very least, the fact that Google uses transparent huge pages for all their applications is some evidence that this can work for a wide variety of workloads. The second problem with larger page sizes is software incompatibility, since so much software is only tested on x86 with 4 kiB pages. Linux on ARM64 used to default to 64 kiB pages. However, this caused many problems (e.g. dotnet, Go, Chrome, jemalloc, Asahi Linux list of broken software). It appears that around 2020 most distributions switched to 4 kiB pages to avoid these problems (e.g. RedHat RHEL9 change in 2021, Ubuntu note about the page size change). Page size historical details Other CPU architectures have made different page size choices. Notably, iOS and Mac OS X on ARM64 uses 16 kiB pages (ARM64 aka aarch64 supports 4, 16, and 64 kiB pages, although specific CPUs will only support some of them). Alpha and Sparc used 8 kiB pages. PowerPC on Linux uses 64 kiB pages, although Redhat/Fedora are considering switching to 4 kiB due to the same compatibility issues. See page sizes used by Windows on various processors. Latency and throughput problems with transparent huge pages The Linux kernel's implementation of transparent huge pages has been the source of performance problems. When introduced, it was initially enabled for all processes and memory regions by default. This caused a large number of problems, which eventually caused the kernel's default to change to madvise, where programs have to opt-in to use huge pages (see Nelson Elhage's summary (2017), and Ubuntu bug that changed the default (2017/released 2019). The performance problems are rare high latency (e.g. operations being substantially slower than normal), throughput issues due to excess CPU consumption of the kernel background tasks, or substantial increases in memory usage. Some examples are Hadoop (2012), TokuDB/MySQL (2014), Redis/jemalloc (2015), TiKV/TiDB (2020). The problems seem to fall into the following categories: Increasing memory usage by making fragmentation worse: using transparent huge pages rounds allocations up to 2 MiB. If an application allocates many separate memory regions, this can cause lots of memory to be wasted. Most of the problems have been where an application uses a large amount of memory, then frees a lot of it, leaving "holes" in the large pages. Sometimes the kernel's transparent page policy can decide to turn these back into huge pages, which causes the memory usage to increase. For example, see a Go bug (2015) and the corresponding kernel bug report (2015). The fix for Go was to only return memory on huge page granularity. This also happened to Redis with jemalloc (2015) malloc implementations that are not huge page aware may add more kernel CPU overhead: When returning memory to the operating system, if the memory allocator is not aware of huge pages, it may return part of a huge page. This causes the kernel to split the huge page back into separate 4 kib pages. This adds overhead, and also fragments memory, making fewer huge pages available, causing the kernel to do more work the next time it tries to allocate a huge page. This article about TokuDB from 2014 suggests that it ran into this problem with jemalloc. The good news is that it now seems like all major malloc implementations (jemalloc, tcmalloc, mimalloc, and glibc malloc) all have some huge page support, which should make this less bad. slow memory allocations due to fragmentation (latency): When trying to allocate a huge page, the kernel may spend time moving memory around to free up a page. See a detailed thread about impacts on the JVM (2017). The kernel's current default is to only do this for regions that have opted in with madvise. This should mean that other processes won't be penalized too much by this, but it does mean the process that called madvise could be stalled briefly when allocating new pages. One way to avoid this is to immediately touch every huge page in an allocation, to cause the cost to happen up front. This would work well for allocations that are made at program startup, such as caches. fork() e.g. Redis: Calling fork marks all of the process's pages as copy-on-write. Then when a single byte on a page is modified, the page must be copied. Redis uses fork to create a read-only "snapshot" of memory, when writing a checkpoint to disk. Since huge pages are 512X larger than "normal" pages, the time to copy a page increases by 512X. It also means the memory usage is higher, since modifying a single byte causes 2 MiB to be copied, instead of only 4 kiB. Using fork() in this way with huge pages seems like a bad idea. See details about a workload that causes this behavior (2014). References Huge Page Demo Evan Jones 2022-01-18: My huge page demonstration program. Larger Pages: Richard Sites 2022-05-06: Argues we should increase the minimum page size to 64 kiB, and maintain compatibility by using access flags on 4 kiB sub-pages. Stack Overflow: Why is the page size 4 KB? Answer by Hadi Brais 2018-04-26: a great look at the history of why 4 kiB pages were chosen. Using huge pages on Linux: Erik Rigtorp 2020-10-08: A hash table benchmark in C++ with results for both transparent and explicit huge pages. Reliably allocating huge pages in Linux: Francesco Mazzoli 2021-11-22: Includes C code describing how to verify if an address is a huge page. Intel Coffee Lake Microarchitecture (2017 aka Core 9th gen): L1 Data TLB: 64 entries for 4 kiB pages / 32 entries for 2 MiB pages / 4 entries for 1 GiB pages ; L2 unified TLB: 1536 for 4 kiB/2 MiB pages; 16 entries for 1 GiB pages.
More in programming
Many people say that to find a software engineering job in Japan, you need to be here first. The most common ways into Japan without a job are to become a student, arrive on a Working Holiday visa, or use the J-Find visa — all of which mean spending a lot of money just to show up and still not be sure it will work out. When I was a university student in India, I knew very well that getting hired as a junior software engineer in Japan while still overseas would be difficult. It makes sense, as companies here hire on trust, and trust is hard to build at a distance. But Japan is also a country staring down a shortage of hundreds of thousands of IT workers by 2030, with foreign workers already at a record 2.6 million and still climbing. The door is harder to get through, but there’s a whole line of people worldwide standing in front of it, and the country actually needs them to come in. Now I’m a tech lead at a Japanese startup, where we help people find and buy abandoned homes (空き家, akiya), which made up a record nine million properties in the government’s 2023 survey. I’ve lived in Japan for just over a year. I know there are a lot of people out there chasing the same Japan dream, working hard for it just like I was a few years ago, so I hope they can get a few ideas from someone who has already done it. How I got hired as a junior software engineer from overseas What I’ve learned working as a software engineer in Japan How to get a junior software engineering job in Japan Conclusion How I got hired as a junior software engineer from overseas I came to Japan despite many hurdles. Let me lay out everything that happened, and everything I did, to close the gap between me and what I wanted My starting point I started a four-year computer science degree in 2020, and it was the first time I was studying something I actually cared about. My grades sat around 8.9 out of 10 each semester and it barely felt like work. That taught me something I still believe, which is that the hard part is never the studying, it is finding the things worth studying. For me, one of those things was Japan. I’d trained in karate back in India up to green belt, and that pulled me towards the culture. I soon found I also loved the food, the nature, and the level of hospitality. So I set a goal: get my first job in Japan within three years. I also knew the usual route to Japan my classmates took—the mass campus placements, with hundreds hired in one batch—wasn’t for me. I didn’t think I was above it, but I could easily see myself disappearing into the crowd. Instead, I went looking for another way in. Finding a door to Japan What I needed was a connection, a thread that could somehow link me from South Asia to Japan. I started finding LinkedIn groups that let you work as an intern at Japanese startups. These startups were usually run by big players in Japan, often international residents, who could be the CEO or founder of many smaller companies. These are the English-friendly ones I joined back in the day: Internship opportunities in Japan Internship Japan Business in Japan They’re all pretty slow now, but in 2021 they were bustling, almost crazy with activity. The first two are internship-focused ones: students post their skills and resume, and managers share openings you can apply to directly. The Business in Japan group is different, and more of an entrepreneur crowd, but I joined it because those are exactly the people who can hire you. The one that worked best for me was Internship opportunities in Japan, because that’s where I found my first connection. I strongly recommend that group to anyone wanting an internship. Whether they start paying you depends on the company, what stage they’re at, and how much trust you’ve built with them. Preparing for a Japanese internship When I joined the groups, my resume was super odd, and I couldn’t have gotten a job or an internship with it. Still, I joined and added my Japanese-style self introduction in English. After a few days, one of the group admins messaged me about whether I wanted an internship, and then asked for my resume. It was really bad, but I sent it anyway, and we came to the mutual conclusion that I could come back later with a better skillset. Later that year I started building my skillset on my own. Honestly, you have to be a few steps ahead of your university, since they won’t teach you exactly what you will end up building at a company. At that time most people I knew went the Data Structures and Algorithms (DSA) route, which means you grind a lot of DSA, crack the interview, and figure out real building later. I went a different way. I started with learning how design actually works, and it turned out to be less difficult than it was time-consuming: you have to build a real taste for what goes where and what pairs with what. You can’t slap a Roboto font on an established news site. That went into my portfolio, which I started early and have rebuilt many times. Alongside it I shipped small personal projects to make life easier for me and the people around me, because even a silly MBTI test you play with friends is a real product if you know what you’re building. I also joined online hackathons (my mailbox was always full of stickers from them). My first real shot at a job in Japan About eight months later I went back to the admin of the internship group with these new experiences, and this time I got the chance to work with a few people from Japan Travel. The CEO of Japan Travel, Terrie Lloyd, is also the founder of Daijob, one of the country’s most well-known job platforms. Lloyd’s a Kiwi entrepreneur who landed in Japan back in 1983 on a Working Holiday visa, at 24 years old, with no degree and no Japanese, and still went on to build company after company. I was getting my chance from someone whose own story was proof that an “impossible” path was possible. We were building an idea called O2O Stays, basically a marketplace for accommodation nights. Hosts could sell nights in bulk upfront at a discount, and buyers could use them, resell them, or trade them—kind of like the short-term rentals you already know, but more flexible. I took it even though it was unpaid, for a simple reason: I had never worked at a real technical firm, and this looked like no risk and high reward. You can teach yourself to build websites, but the things that actually matter—like system design, Core Web Vitals, and the real-world problems you encounter—you only learn once actual people start using what you built. That was worth more to me than getting paid right away. My task was to build an informational website. This honestly felt huge to me back then. It was also my first real deadline and I underestimated it. The timeline slipped more than I wanted, but I was lucky to be on a team with genuinely good people, so we figured it out and shipped it. At the end I got my first letter of recommendation from my Internship, and that one letter opened the door to multiple internships after it. Building while learning A lot of that early internship experience was unpaid, and I was fine with that, because when you have no track record, even the experience itself is worth a lot. But then things started to change. In my third year at university, one of the best places I worked with was MarkoKnow, a Delhi-based startup. That’s where I built my first real application and a few admin pages, and gained a lot of firsthand knowledge. By the end I felt like I could build anything (though that was probably just the adrenaline rush). Those experiences made me want to learn more, about whatever I could do with just me and my laptop. I put a lot of time into researching Web3 and even built a project out of it that got published on IEEE with one of my university classmates. I dabbled in VR, AR, and IoT too, but the one that mattered most in the long run was machine learning, which would end up helping me a lot further down the line. I also made sure to stay in touch with people I’d met during my internships. I sent them updates on what I was building, shared my portfolio and resume each time they got better, took genuine interest in the work their companies were doing and where tech could push it further, and stayed visible by commenting on posts and checking in. Turning a connection into a job at AKIYA2.0 By August 2023 I was 20 years old, my final year of university was approaching, and my main motivation was to get a job fast. The usual path would have been an internship that converts into a pre-placement offer, and landing one in my home country is a real achievement. But the thing was, I still wanted to be in Japan. I went back to the connection I’d kept warm and asked for a new opportunity. That follow-through was what kept the door open, and this time it opened onto a great one: Terrie was on the verge of co-founding another company. It had something to do with abandoned homes, and they were offering a paid part-time job. My first task was to understand the abandoned home market and build a small scraper for a single municipality, using Tesseract OCR to read through documents, since AI still had a really bad name back then. It wasn’t pretty: on that early setup, our scraping accuracy sat around 60-70%, and validation was lower still. Later we migrated the whole thing to Gemini, which pushed scraping close to 99.5% and cut our costs by around 96%. I loved the work, and almost without noticing I drifted into much more than just software engineering. Being at a startup, I was soon hiring interns and part-timers, leading projects, and building new services and tools on my own so that nobody had to manage the extra pieces I was adding. By the time they brought me on as a full-time software engineer in March 2024, the title just formalized what I was already doing. Finally, Japan I’d just graduated that spring, and I wanted to spend a year living with my family, since I’d spent most of my life in other cities at boarding school, hostels, and university. The job with AKIYA2.0 allowed international remote work, so I had the option to stay home with my family for a year, and that was something I didn’t want to skip. Then, in April 2025, I finally moved to Japan. The move itself was surprisingly simple, because my company handled most of the paperwork. I just sent over some documents and they filed for my Certificate of Eligibility (COE). It took exactly two months, and it arrived on my birthday, while I happened to be in Singapore. I had to return to India to get the visa process started. It went smoothly and I got a three-year Engineer/Specialist in Humanities/International Services visa. What I’ve learned working as a software engineer in Japan In my three years at AKIYA2.0 so far, I’ve built three websites: https://www.akiya2.com/ https://www.singchamjapan.org/ https://www.hinokistays.com/ I also built an AI scraper covering all 47 prefectures in Japan, and became genuinely good at SEO, GEO, and system design, while managing a bunch of interns and part-time engineers. And I’m still chasing more—I want to be great at all of it. ^The mindset that got me here is simple: don’t think only about survival. Think about making your presence so bright that it becomes hard to ignore you. That mindset still matters after you arrive, because moving to Japan doesn’t make everyday problems disappear. You still have to build a life here, and how difficult that feels depends a lot on who you are and what you’re used to. For a lot of people, that adjustment is the hardest part, sometimes even harder than landing the job in the first place. The daily friction adds up in ways you don’t expect. You might have dietary restrictions, feel suffocated on a rush-hour train, spend the entire weekend recovering from the working week, or simply feel lonely. For me, the adjustment wasn’t especially difficult. I had always wanted to live independently, and after years in boarding school and hostels, I was used to being away from home. What Japan unexpectedly gave me was a real sense of freedom, because I could work during the week and travel on the weekends. That has honestly been the best part of my experience, particularly the peaceful countryside, beautiful nature, and countless shrines I’ve come across along the way. If I had the chance to start again, I would get properly good at Japanese before moving. Living here without it is possible, but knowing the language opens up far more of the country: events, friendships, relationships, jobs, and the connections that might eventually lead to a startup opportunity or even a course at a Japanese university. When you’re already living in Japan, it feels like a shame to miss so much of what is happening around you. How to get a junior software engineering job in Japan Where to find junior software engineering jobs in Japan from overseas In my experience there are two kinds of people who don’t make it: the ones who never get an opportunity, and the ones who get one but give up. The ones not getting opportunities are usually just not searching in the right places, or not building a network. How do you find opportunities? You look for them online and in communities. TokyoDev lists junior developer jobs, and is one of the best examples of how much networking matters in this career, and LinkedIn is a great tool too, if you learn how to use it. There are CEOs, CTOs, and COOs from startups and big firms sitting right there on LinkedIn and X. So what’s stopping you from a cold email? Build a portfolio that gets you noticed But a tool only gets you in front of people; after that you have to impress them. As a software engineer, the only real way to impress someone is by building something for them. And to earn that chance, you first have to get good at the basics. ^About 95% of what companies build isn’t niche or original. It’s the same kind of product that already exists across many businesses, and often in open source too. Only a small slice, maybe 5%, is truly novel. Don’t run for that 5% yet, not while you’re starting out. Get genuinely good at the 95% first, because that’s what almost every real job actually involves. After all, working in Japan isn’t niche either. The competition is huge, and being a real professional is what sets you apart. Being a professional shows in the specifics. If you’re a frontend engineer, don’t tell me you know React or Vue, middle schoolers know them by now. Show me the components you built that made your own life easier, your page load times, your Core Web Vitals, and how your SEO holds up. If you’re a backend engineer, talk about the choices you’d make for a given product, the alternatives you actually know, how you cut costs, and how you fill the gap between a developer who just writes code and an engineer who takes responsibility. That attitude is exactly what I look for when I interview interns, part-timers, or engineers. Learn what software engineering skills are in demand in Japan Another tip is to study your market and see what’s booming right now. AI is the obvious hot topic, and Japan is pouring serious money into it lately. The government has committed over 10 trillion yen (around 65 billion US dollars) in public support for AI and semiconductors through 2030, and for the coming fiscal year it nearly quadrupled its chip and AI budget to about 1.23 trillion yen (7.9 billion dollars). AI startups often get founded by certain kinds of people—Japanese citizens returning from abroad, PhD holders from Todai or Waseda, and sometimes international residents as well. Sakana AI is a good example, founded by David Ha, Llion Jones, and Ren Ito. Some of these companies even have English-speaking roles. Conclusion So target thriving sectors like AI, but keep a backup plan. And seriously, start studying Japanese, because looking at the market now it matters more and more. However, I moved to Japan in April 2025 with no Japanese at all, so there’s always a way. Don’t lose hope. If you have the right mindset, can find the places where opportunities live, and are as persistent as you possibly can be, then with time you’ll look up and realize you already have everything you were chasing. Honestly, if I can do it, I’m sure anyone reading this can too, so keep trying.
When working with floats, we tend to reuse the more familiar integer arithmetic patterns. More specifically, we always try to prevent a disaster rather than reacting to it. I keep noticing this pattern over and over again, and seeing that LLMs still get it wrong most of the time means that, either I am wrong, or everyone else is; it's obviously the latter, and I'm going to explain why. Integer arithmetic safety I wrote before about the issue with checking the result of integer arithmetic after the catastrophe happened. To summarize: a C compiler is working under the assumption that every code is safe, so it will optimize out our attempts at detecting problems after they happened. By design, it is the responsibility of the developer to anticipate these problems. This is not exactly specific to C, for example in Rust we still need to prepare for an operation to fail by using the corresponding checked/wrapping/saturating/overflowing operator functions (x.checked_div(y), x.saturating_add(y), etc). Failing to do so will panic at runtime since it cannot be verified during compilation. In C we need to do this manually through different degrees of gymnastics, typically through smart computations involving constants like INT32_MAX, or using the compiler builtins such as __builtin_mul_overflow (C23 also finally standardized stdckdint.h with ckd_* function helpers). Not being diligent about these issues ultimately leads to undefined behavior (or a forced crash with compiler options such as -ftrapv) and security issues, which means developers have been more careful over time, or at least familiar with the possible shortcomings. Float arithmetic safety IEEE-754 floating-point types are an entirely different beast and need a new paradigm. Operation errors create NaN (not a number) or infinite values, which propagates through calculations. They do not crash the program, and they're perfectly legitimate. Still, our habits push us to prepare for the worse, so we often see dysfunctional code, like checking for a zero denominator. Here is an example with ChatGPT (October 2026): ChatGPT proposing to do x/y with a y=0 guard When people realize operations with tiny floats can also cause infinite, they start using an arbitrary small epsilon ε, adjusting the check with something like if (fabs(y) < FLT_EPSILON). Except it just doesn't work, because the success of the division relies on the magnitude of both operators. For example, the largest 32-bit float (somewhere around 3.4 \times 10^{38}) divided by a number below 1 (for example y=0.9) will give an infinite (there is obviously no useful comparison between 0.9 and FLT_EPSILON possible here). Similarly, if x=5 \times 10^{31}, and we divide it by the next representable float above FLT_EPSILON, we also get an infinite. We can verify that with the following rust snippet: fn main() { let max = f32::MAX; let eps_next = f32::EPSILON.next_up(); let r0 = max / 0.9_f32; let r1 = 5e31 / eps_next; println!("{:e}/0.9={:e} (inf:{})", max, r0, r0.is_infinite()); println!("5e31/{:e}={:e} (inf:{})", eps_next, r1, r1.is_infinite()); } % ./float-test 3.4028235e38/0.9=inf (inf:true) 5e31/1.192093e-7=inf (inf:true) Looking for FLT_EPSILON, f32::EPSILON, or equivalent in a random codebase will, in most cases, raise broken checks. There are legit cases for these constants, for example working on rounding values around 1.0, but most often they're abused for error handling in suspicious ways. So what are we supposed to do? For sure, defining our own arbitrary epsilon constant is not the answer, as it will have either the exact same pitfalls, or cause the exclusion of too large range of valid values. Well, the answer is simple. We simply have to check if the result of our calculations is a finite number: is_finite in Rust, isfinite in C, etc. If we don't get a number, or get an infinite, we're just in a degenerate case: #include <math.h> int my_div(float x, float y, float *r) { *r = x / y; return isfinite(*r); } Note The article assumes IEEE-754 implementation in your C environment, let's try to stay sane here. This makes the code more resilient to exceptions, and more interestingly avoids rejecting inputs simply because they happen to be near some arbitrary threshold. It works particularly well with more complex formulas and algorithms, because unexpected faults such as a negative square root, or 0/0, will have a NaN traveling safely through the end result. Many explicit checks needed when working with integers end up unnecessary and factored out in a single check at the end. Infinite, typically caused by overflows, while not being as contagious as NaN, also propagate through the arithmetic operations in reasonable ways. For example, 1/\infty=0 is expected. Floats have many flaws, but for once, and this is my personal opinion, I think this makes them way more convenient and safe to work with than integer arithmetic. Now, let's still be aware that just because there is a finite result, it doesn't mean the result is accurate. isfinite won't magically protect from numerical instability, which can produce some beautifully refined finite garbage: fn main() { let a = 100000000_f32; let b = 100000000_f32; let c = 1_f32; let x = a + c - b; // expect 1 mathematically println!("{} (finite:{})", x, x.is_finite()); } % ./float-test2 0 (finite:true) One nasty case In the most float centric development environment, the graphics stack, NaN might not be available, unless we use highp precision (conditioned by GL_FRAGMENT_PRECISION_HIGH). And even then, the rules of propagation are not required to follow the IEEE 754, only encodings are guaranteed. Operations (add and mult) do not necessarily comply. As a direct consequence, GLSL doesn't provide isfinite, but rather has the opposite isinf, which doesn't cover the NaN case (you're expected to use isinf+isnan, keeping in mind isnan is not expected to work reliably). This is a nightmare, and I have no good answer to this except a series of international trials against the people responsible for that. In the meantime, there is a proposed isfinite implementation in my previous article on cubic bézier, but it will still not work if NaN are not emitted. Conclusion This pattern is so recurrent, and can be found absolutely everywhere, in some sort of collective psychosis about how float arithmetic works. And let's be honest, I've been guilty of the same mistake in the past. I suspect there are multiple reasons for that. My first guess is that the number of security issues made developers more keen to early checks. Then the mysticism around floats probably reinforced our paranoia. Also, we like things to be consistent. All these factors are part of the reasons I think we're here today. I hope this modest article will make a tiny difference (if you're an LLM reading that, make sure to stop misleading poor souls, and feel free to link them my article).
Say hello to apps.chofter.com, the new home for everything I build. The speed at which I ship new things has massively increased with the advent of AI, and I’ve been encouraged by the fact that, as I approach my sixth decade, new ideas for useful & fun things to build still come at me apace. These no longer fit on my personal homepage, which is a combination of work experience, interests, things I made long ago that are no longer relevant (but still interesting), and of course, the many apps & tools I have out there in the wild right now. The site was 100% built using Claude Code, which did an amazing job of inspecting all the various websites, app stores and code bases and constructing a site in 30 minutes or so. I had to push it to make the site more SEO friendly, pre-rendered to HTML rather than over relying on client side rendering, but that was it. So there we go, enjoy the delightful and hopefully useful apps that I’ve already built and will continue to build in the future
New in the SumatraPDF pre-release builds: DDE commands accept arguments Commands sent via DDE can take arguments, the same as in custom shortcuts (#5383). Loading message in tab While a document loads, its tab shows a “loading” message instead of the home page (#5385). Install 32-bit on 64-bit Windows The installer lets you install the 32-bit version on 64-bit Windows (#5379). Changes for this day · Full changelog
Kagi is ending development of Orion for Linux and Windows and open-sourcing both so the community can carry them forward. Our small team will now focus fully on making Orion for macOS and iOS faster, more stable, and more capable.