2026-03-05 22:40:58

The Internet relies on text formats. Thus, we spend a lot of time producing and consuming data encoded in text.
Your web pages are HTML. The code running in them is JavaScript, sent as text (JavaScript source), not as already-parsed code. Your emails, including their attachments, are sent as text (your binary files are sent as text).
It does not stop there. The Python code that runs your server is stored as text. It queries data by sending text queries. It often gets back the answer as text that must then be decoded.
JSON is the universal data interchange format online today. We share maps as JSON (GeoJSON).
Not everything is text, of course. There is no common video or image format that is shared as text. Transmissions over the Internet are routinely compressed to binary formats. There are popular binary formats that compete with JSON.
But why is text dominant?
It is not because, back in the 1970s, programmers did not know about binary formats.
In fact, we did not start with text formats. Initially, we worked with raw binary data. Those of us old enough will remember programming in assembly using raw byte values.
Why text won?
1.Text is efficient.
In the XML era, when everything had to be XML, there were countless proposals for binary formats. People were sometimes surprised to find that the binary approach was not much faster in practice. Remember that many text formats date back to an era when computers were much slower. Had text been a performance bottleneck, it would not have spread. Of course, there are cases where text makes things slower. You then have a choice: optimize your code further or transition to another format. Often, both are viable.
It is easy to make wrong assumptions about binary formats, such as that you can consume them without any parsing or validation. If you pick up data from the Internet, you must assume that it could have been sent by an adversary or someone who does not follow your conventions.
2.Text is easy to work with.
If you receive text from a remote source, you can often transform it, index it, search it, quote it, version it… with little effort and without in-depth knowledge of the format. Text is often self-documenting.
In an open world, when you will never speak with the person producing the data, text often makes everything easier and smoother.
If there is an issue to report and the data is in text, you can usually copy-paste the relevant section into a message. Things are much harder with a binary format.
2026-03-01 03:21:39

We locate web content using special addresses called URLs. We are all familiar with addresses like https://google.com. Sometimes, URLs can get long and they can become difficult to read. Thus, we might be tempted to format them
like so in HTML using newline and tab characters, like so:
<a href="https://lemire.me/blog/2026/02/21/
how-fast-do-browsers-correct-utf-16-strings/">my blog post</a>
It will work.
Let us refer to the WHATWG URL specification that browsers follow. It makes two statements in sequence.
Notice how it reports an error if there is a tab or newline character, but continues anyway? The specification says that A validation error does not mean that the parser terminates and it encourages systems to report errors somewhere. Effectively, the error is ignored although it might be logged. Thus our HTML is fine in practice.
The following is also fine:
<a href="https://go
ogle.c
om" class="button">Visit Google</a>
You can also use tabs. But you cannot arbitrarily insert any other whitespace.
Yet there are cases when you can use any ASCII whitespace character: data URLs. Data URLs (also called data URIs) embed small files—like images, text, or other content—directly inside a URL string, instead of linking to an external resource. Data URLs are a special kind of URL and they follow different rules.
A typical data URL might look like data:image/png;base64,iVBORw0KGgoAAAANSUhEUg... where the string iVBORw0KGgoAAAANSUhEUg... is the binary data of the image that has been encoded with base64. Base64 is a text format that can represent any binary content: we use 64 ASCII characters so that each character encodes 6 bits. Your binary email attachments are base64 encoded.
On the web, when decoding a base64 string, you ignore all ASCII whitespaces (including the space character itself). Thus you can embed a PNG image in HTML as follows.
<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA
QAAAAECAIAAAAmkwkpAAAAEUl
EQVR4nGP8z4AATEhsPBwAM9EB
BzDn4UwAAAAASUVORK5CYII=" />
This HTML code is valid and will insert a tiny image in your page.
But there is more. A data URL can also be used to insert an SVG image. SVG (Scalable Vector Graphics) is an XML-based vector image format that describes 2D graphics using mathematical paths, shapes, and text instead of pixels.
The following should draw a very simple sunset:
<img src='data:image/svg+xml,
<svg width="200" height="200"
xmlns="http://www.w3.org/2000/svg">
<rect width="100%" height="100%" fill="blue" />
<!-- the sky -->
<circle cx="100" cy="110" r="50" fill="yellow" />
<!-- the sun -->
<rect x="0" y="120" width="200" height="80" fill="brown" />
<!-- the ground -->
</svg>' />
Observe how I was able to format the SVG code so that it is readable.
Further reading: Nizipli, Y., & Lemire, D. (2024). Parsing millions of URLs per second. Software: Practice and Experience, 54(5), 744-758.
2026-02-22 04:07:17

JavaScript represents strings using Unicode, like most programming languages today. Each character in a JavaScript string is stored using one or two 16-bit words. The following JavaScript code might surprise some programmers because a single character becomes two 16-bit words.
> t="🧰" '🧰' > t.length 2 > t[0] '\ud83e' > t[1] '\uddf0'
The convention is that \uddf0 is the 16-bit value 0xDDF0 also written U+DDF0.
The UTF-16 standard is relatively simple. There are three types of values. high surrogates (the range U+D800 to U+DBFF), low surrogates (U+DC00 to U+DFFF), and all other code units (U+0000–U+D7FF together with U+E000–U+FFFF). A high surrogate must always be followed by a low surrogate, and a low surrogate must always be preceded by a high surrogate.
What happens if you break the rules and have a high surrogate followed by a high surrogate? Then you have an invalid string. We can correct the strings by patching them: we replace the bad values by the replacement character (\ufffd). The replacement character sometimes appears as a question mark.
To correct a broken string in JavaScript, you can call the toWellFormed method.
> t = '\uddf0\uddf0' '\uddf0\uddf0' > t.toWellFormed() '��'
How fast is it?
I wrote a small benchmark that you can test online to measure its speed. I use broken strings of various sizes up to a few kilobytes. I run the benchmarks on my Apple M4 processor using different browsers.
| Browser | Speed |
|---|---|
| Safari 18.6 | 1 GiB/s |
| Firefox 147 | 3 GiB/s |
| Chrome 145 | 15 GiB/s |
Quite a range of performance! The speed of other chromium-based browsers (Brave and Edge) is much the same as Chrome.
I also tested with JavaScript runtimes.
| Engine | Speed |
|---|---|
| Node.js v25.5.0 | 16 GiB/s |
| Bun 1.3.9 | 8.4 GiB/s |
Usually Bun is faster than Node, but in this instance, Node is twice as far as Bun.
Thus, we can correct strings in JavaScript at over ten gigabytes per second if you use Chromium-based browsers.
2026-02-16 04:02:29

When programming, we need to allocate memory, and then deallocate it. If you program in C, you get used to malloc/free functions. Sadly, this leaves you vulnerable to memory leaks: unrecovered memory. Most popular programming languages today use automated memory management: Java, JavaScript, Python, C#, Go, Swift and so forth.
There are essentially two types of automated memory managements. The simplest method is reference counting. You track how many references there are to each object. When an object has no more references, then we can free the memory associated with it. Swift and Python use reference counting. The downside of reference counting are circular references. You may have your main program reference object A, then you add object B which references object A, and you make it so that object A also reference object B. Thus object B has one reference while object A has two references. If your main program drops its reference to object A, the both objects A and B still have a reference count of one. Yet they should be freed. To solve this problem, you could just visit all of your objects to detect which are unreachable, including A and B. However, it takes time to do so. Thus, the other popular approach of automated memory management: generational garbage collection. You use the fact that most memory gets released soon after allocation. Thus you track young objects and visit them from time to time. Then, more rarely, you do a full scan. The downside of generational garbage collection is that typical implementations stop the world to scan the memory. In many instances, your entire program is stopped. There are many variations on the implementation, with decades of research.
The common Python implementation has both types: reference counting and generational garbage collection. The generational garbage collection component can trigger pauses. A lot of servers are written in Python. It means that your service might just become unavailable for a time. We often call them ‘stop the world’ pauses. How long can this pause get?
To test this out, I wrote a Python function to create a classical linked list:
class Node:
def __init__(self, value):
self.value = value
self.next = None
def add_next(self, node):
self.next = node
def create_linked_list(limit):
""" create a linked list of length 'limit' """
head = Node(0)
current = head
for i in range(1, limit):
new_node = Node(i)
current.add_next(new_node)
current = new_node
return head
And then I create one large linked list and then, in a tight loop, we create small linked lists that are immediately discarded.
x = create_linked_list(50_000_000)
for i in range(1000000):
create_linked_list(1000)
A key characteristic of my code is the 50 million linked list. It does not get released until the end of the program, but the garbage collector may still examine it.
And I record the maximum delay between two iterations in the loop (using time.time()).
How bad can it get? The answer depends on the Python version. And it is not consistent from run-to-run. So I ran it once and picked whatever result I got. I express the delay in milliseconds.
| python version | system | max delay |
|---|---|---|
| 3.14 | macOS (Apple M4) | 320 ms |
| 3.12 | Linux (Intel Ice Lake) | 2,200 ms |
Almost all of this delay (say 320 ms) is due to the garbage collection. Creating a linked list with 1000 elements takes less than a millisecond.
How long is 320 ms? It is a third of a second, so it is long enough for human beings to notice it. For reference, a video game drawing the screen 60 times per second has less than 17 ms to draw the screen. The 2,200 ms delay could look like a server crash from the point of view of a user, and might definitely trigger a time-out (failed request).
I ported the Python program to Go. It is the same algorithm, but a direct comparison is likely unfair. Still, it gives us a reference.
| go version | system | max delay |
|---|---|---|
| 1.25 | macOS (Apple M4) | 50 ms |
| 1.25 | Linux (Intel Ice Lake) | 33 ms |
Thus Go has pauses that are several times shorter than Python, and there is no catastrophic 2-second pause.
Should these pauses be a concern? Most Python programs do not create so many objects in memory at the same time. Thus you are not likely to see these long pauses if you have a simple web app or a script. Python gives you a few options, such as gc.set_threshold and gc.freeze which could help you tune the behaviour.
Video
2026-02-15 23:19:31

Much of the West has been economically stagnant. Countries like Canada have failed to improve their productivity and standard of living as of late. In Canada, there has been no progress in Canadian living standards as measured by per-person GDP over the past five years. It is hard to overstate how anomalous this is: the USSR collapsed in part because it could only sustain a growth rate of about 1%, far below what the West was capable of. Canada is more stagnant than the USSR.
Late in 2022, some of us got access to a technical breakthrough: AI. In three years, it has become part of our lives. Nearly all students use AI to do research or write essays.
Dallas Fed economists projected the most credible effect that AI might have on our economies: AI should help reverse the post-2008 slowdown and deliver higher living standards in line with historical technological progress.
It will imply a profound, rapid but gradual transformation of our economy. There will still be teachers, accountants, and even translators in the future… but their work will change as it has changed in the past. Accountants do far less arithmetic today; that part of their work has been replaced by software. Even more of their work is about to be replaced by software, thus improving their productivity further. We will still have teachers, but all our kids, including the poorest ones, will have dedicated always-on tutors: this will not be just available in Canada or the USA, but everywhere. It is up to us to decide who is allowed to build this technology.
AI empowers the individual. An entrepreneur with a small team can get faster access to quality advice, copywriting, and so forth. Artists with an imagination can create more with fewer constraints.
I don’t have to prove these facts: they are fast becoming obvious to the whole world.
New jobs are created. Students of mine work as AI specialists. One of them helps build software providing AI assistance to pharmacists. One of my sons is an AI engineer. These are great jobs.
The conventional explanation for Canada’s stagnation is essentially that we have already harvested all the innovation we are ever going to get. The low-hanging fruit has been picked. Further progress has become inherently difficult because we are already so advanced; there is simply not much room left to improve. In this view, there is no need to rethink our institutions. Yet a sufficiently large breakthrough compels us to reconsider where we stand and what is still possible. It forces us to use our imagination again. It helps renew the culture.
We often hear claims that artificial intelligence will consume vast amounts of energy and water in the coming years. It is true that data centers, which host AI workloads along with many other computing tasks, rely on water for cooling.
But let’s look at the actual water numbers. In 2023, U.S. data centers directly consumed roughly 17.4 billion gallons of water—a figure that could potentially double or quadruple by 2028 as demand grows. By comparison, American golf courses use more than 500 billion gallons every year for irrigation, often in arid regions where this usage is widely criticized as wasteful. Even if data-center water demand were to grow exponentially, it would take decades to reach the scale of golf-course irrigation.
On the energy side, data centers are indeed taking a larger share of electricity demand. According to the International Energy Agency’s latest analysis, they consumed approximately 415 TWh in 2024—about 1.5% of global electricity consumption. This is projected to more than double to around 945 TWh by 2030 (just under 3% of global electricity). However, even this rapid growth accounts for less than 10% (roughly 8%) of the total expected increase in worldwide electricity demand through 2030. Data centers are therefore not the main driver of the much larger rise in overall energy use.
If we let engineers in Australia, Canada, or Argentina free to innovate, we will surely see fantastic developments.
You might also have heard about the possibility that ChatGPT might decide to kill us all. Nobody can predict the future, but you are surely more likely to be killed by cancer than by a rogue AI. And AI might help you with your cancer.
We always have a choice. Nations can try to regulate AI out of existence. We can set up new government bodies to prevent the application of AI. This will surely dampen the productivity gains and marginalize some nations economically.
The European Union showed it could be done. By some reports, Europeans make more money by fining American software companies than by building their own innovation enterprises. Countries like Canada have economies dominated by finance, mining and oil (with a side of Shopify).
If you are already well off, stopping innovation sounds good. It’s not if you are trying to get a start.
AI is likely to help young people who need it so much. They, more than any other group, will find it easier to occupy the new jobs, start the new businesses.
If you are a politician and you want to lose the vote of young people: make it difficult to use AI. It will crater your credibility.
It is time to renew our prosperity. It is time to create new exciting jobs.
References:
Wynne, M. A., & Derr, L. (2025, June 24). Advances in AI will boost productivity, living standards over time. Federal Reserve Bank of Dallas.
Fraser Institute. (2025, December 16). Canada’s recent economic growth performance has been awful.
DemandSage. (2026, January 9). 75 AI in education statistics 2026 (Global trends & facts).
MIT Technology Review. (2026, January 21). Rethinking AI’s future in an augmented workplace.
Davis, J. H. (2025). Coming into view: How AI and other megatrends will shape your investments. Wiley.
Choi, J. H., & Xie, C. (2025, June 26). AI is reshaping accounting jobs by doing the boring stuff. Stanford Graduate School of Business.
International Energy Agency. (n.d.). Energy demand from AI.
University of Colorado Anschutz Medical Campus. (2025, May 19). Real talk about AI and advancing cancer treatments.
International Energy Agency. (2025). Global energy review 2025.
2026-02-09 04:11:32

When programming, we chain functions together. Function A calls function B. And so forth.
You do not have to program this way, you could write an entire program using a single function. It would be a fun exercise to write a non-trivial program using a single function… as long as you delegate the code writing to AI because human beings quickly struggle with long functions.
A key compiler optimization is ‘inlining’: the compiler takes your function definition and it tries to substitute it at the call location. It is conceptually quite simple. Consider the following example where the function add3 calls the function add.
int add(int x, int y) {
return x + y;
}
int add3(int x, int y, int z) {
return add(add(x, y), z);
}
You can manually inline the call as follows.
int add3(int x, int y, int z) {
return x + y + z;
}
A function call is reasonably cheap performance-wise, but not free. If the function takes non-trivial parameters, you might need to save and restore them on the stack, so you get extra loads and stores. You need to jump into the function, and then jump out at the end. And depending on the function call convention on your system, and the type of instructions you are using, there are extra instructions at the beginning and at the end.
If a function is sufficiently simple, such as my add function, it should always be inlined when performance is critical. Let us examine a concrete example. Let me sum the integers in an array.
for (int x : numbers) {
sum = add(sum, x);
}
I am using my MacBook (M4 processor with LLVM).
| function | ns/int |
|---|---|
| regular | 0.7 |
| inline | 0.03 |
Wow. The inline version is over 20 times faster.
Let us try to see what is happening. The call site of the ‘add’ function is just a straight loop with a call to the function.
ldr w1, [x19], #0x4
bl 0x100021740 ; add(int, int)
cmp x19, x20
b.ne 0x100001368 ; <+28>
The function itself is as cheap as it can be: just two instructions.
add w0, w1, w0
ret
So, we spend 6 instructions for each addition. It takes about 3 cycles per addition.
What about the inline function?
ldp q4, q5, [x12, #-0x20]
ldp q6, q7, [x12], #0x40
add.4s v0, v4, v0
add.4s v1, v5, v1
add.4s v2, v6, v2
add.4s v3, v7, v3
subs x13, x13, #0x10
b.ne 0x1000013fc ; <+104>
It is entirely different. The compiler has converted the addition to advanced (SIMD) instructions processing blocks of 16 integers using 8 instructions. So we are down to half an instruction per integer (from 6 instructions). So we use 12 times fewer instructions. On top of having fewer instructions, the processor is able to retire more instructions per cycle, for a massive performance boost.
What if we prevented the compiler from using these fancy instructions while still inlining? We still get a significant performance boost (about 10x faster).
| function | ns/int |
|---|---|
| regular | 0.7 |
| inline | 0.03 |
| inline (no SIMD) | 0.07 |
Ok. But the add function is a bit extreme. We know it should always be inlined. What about something less trivial like a function that counts the number of spaces in a string.
size_t count_spaces(std::string_view sv) {
size_t count = 0;
for (char c : sv) {
if (c == ' ') ++count;
}
return count;
}
If the string is reasonably long, then the overhead of the function call should be negligible.
Let us pass a string of 1000 characters.
| function | ns/string |
|---|---|
| regular | 111 |
| inline | 115 |
The inline version is not only not faster, but it is even slightly slower. I am not sure why.
What if I use short strings (say between 0 and 6 characters)? Then the inline function is measurably faster.
| function | ns/string |
|---|---|
| regular | 1.6 |
| inline | 1.0 |
Takeaways:
Note: My source code is available.