In the olden days, the early pioneers of modern computing had a seemingly insurmountable obstacle ahead of them. They had to figure out how computers were to effectively and efficiently represent the decimal number system. They eventually agreed upon using ones and zeroes to emulate integers. Although it was strange to use a base-2 number system to emulate a base-10 number system, this design proved to be an ingenious one due to the on-and-off nature of ones and zeroes that practically coincided with the one-and-off behavior of transistors, logic gates, arithmetic units, and processing modules. It was basically a match made in heaven.
Problems arose when they had to consider fractions, precision, and negative numbers. Clever innovations and workarounds, such as the sign bit and the two's complement, ultimately led to the formation of the IEEE 754 standard in 1985. The binary format of numbers was designed to liken scientific notation so that it could store and efficiently perform arithmetic operations on a wider range of numbers with greater decimal precision.
Despite its computational flaws and limitations (such as the infamous
0.1 + 0.2 != 0.3), the standard serves as a viable solution/compromise for many computer manufacturers and language designers. In fact, the standard (and its succeeding revisions) is so well-engineered that we often take it for granted. Most of the computers and programming languages we know today either fully adopt or somehow derive from the IEEE 754 standard. They cannot function as efficiently and precisely without such a standardized method of handling floating-point arithmetic.
Though beginner-friendly and flexible for many use cases, having all numbers as signed 64-bit (double-precision) floating-point numbers presents quite a huge problem from a memory optimization standpoint. Not all use cases require such range and precision over numbers. Allocating 52 bits for the mantissa, 11 bits for the exponent, and 1 for the sign bit is definitely an overkill for the common usage. 8 bytes are simply too much for a small, positive integer—like the
length property of an array for example—that could easily be represented by 1 or 2 bytes.
C++ gives us greater freedom over the size of our numbers with
double types and their respective modifiers (
long). When I first discovered this, my inner nerd was immediately excited by the amount of control I had. Suddenly, I was released from the shackles of dynamically-typed languages. Suddenly, I had the ability to use as little memory as I deemed safe and necessary.
Since I rarely use fractions and negative numbers in my programs, the
unsigned short int is my default number type. Unless an API/implementation requires otherwise or I find a real possibility of integer overflow, I have no particular reason to upgrade to a larger integer type. Call me a pedant for needlessly "optimizing", but my inner nerd just finds a lot of satisfaction in saving as much memory as I can. Although it is quite verbose to type
unsigned short int everywhere, it is nonetheless a great feeling to know that I have saved 6 bytes of memory for not being forced to use double-precision floating-point numbers.
Just when I thought that 2-byte
unsigned short int numbers were the ultimate solution to my obsession with memory optimization, Rust comes into my life and slaps me across the face with unsigned 1-byte integers (type-annotated as
Sure, one can argue that C++ also has a construct for a "byte-sized" integer, but semantically speaking, a
char is meant to be used as a character, not an integer. Declaring an integer as an
unsigned char will surely get the job done for me, but without explicit documentation, it horribly fails to communicate my intent to interpret it as an integer. Simply put, Rust provides a semantically superior construct for "byte-sized" integers compared to C++. As an added bonus, it is also much more convenient to type
But then you may ask, "What is the point of storing an integer that overflows beyond
255?" Honestly, unless you are using it—for example—to store the value of a color component (as in the RGB color model), there is no clear advantage in using a "byte-sized" integer over an
unsigned short int (or a
u16 in Rust). You have to be really pedantic (or quite nerdy) like me to even consider using it.
// C++ 😐 // Quite memory-efficient (2 bytes) but very verbose const unsigned short int num = 1;
// Rust ✅ // Quite concise and very memory-efficient (1 byte) let num: u8 = 1;
At the end of the day, one can argue that these types of "micro-optimizations" do not have an impact on the overall performance of a program thanks to the greatness of modern CPUs and RAM cards. Yes, I completely agree with that argument, but I did not write this article to assert that "we must always use
u8 integers whenever possible". I wrote this article to express my gratitude towards number types for the freedom it gives me as a programmer over memory allocations.
In today's day and age where high-level, dynamically-typed languages rule the software development scene, this degree of control over memory has become a thing of the past... and rightfully so! Nowadays, it has become unnecessarily tedious
and rather unproductive to worry about the nuances of memory management, especially with the recent rise in popularity of the "agile development" philosophy.
Nevertheless, I do not allow this to come in the way of finding joy in the little things of life. For me, I find a special satisfaction in maximizing the bits, bytes, and nibbles I have at my disposal. It's not because I want to be unproductive or anything like that; some part of me just pats me in the back when I know I did my best to manage the limited resources I have.
Perhaps this obsession for resource optimization comes from the fact that I was surrounded by low-end devices growing up as a child. I vividly remember how I couldn't bare spending another second waiting for a program to finally become responsive. From then on, I have made a promise to myself that I will never write software that would make people experience the excruciatingly long waiting times I have experienced during my childhood.
And for that reason, I say "thank you" to number types—namely the
unsigned short int of C++ and the
u8 of Rust—for allowing me to fulfill my lifelong devotion to optimization and for being by my side whenever I need my daily dose of memory optimization to cheer me up.
🥂 Cheers to number types! 🥂