Small String Optimization (SSO) in C++

Small String optimization SSO in C++

Introduction

Small string optimization (SSO) is a mechanism present in many programming languages and modern compilers. Its purpose is to reduce memory allocations on the heap. This optimization is particularly crucial in embedded environments and low-latency systems, where we must be vigilant about both our application’s memory consumption and code performance.

In the cppcon talk “The strange details of std::string at Facebook”, Nicholas Ormrod mentioned that std::string is the most frequently included file in the Facebook database, representing 18% of the CPU time spent in the STL.

Memory allocations are slow due to the numerous steps involved in the process of allocating memory on the heap. Typically, when working with strings, this memory is swiftly deallocated after usage, such as when printing text or assigning names to local (stack) objects.

The most frequently used string in C++ is the empty string. This string is generated when we perform default initialization on a string, and it ocuppies1 byte, which is the null terminator.

How does it works?

Before GCC 5.0, Small String Optimization (SSO) was not implemented. The std::string had a size of 8 bytes because both the size and the capacity were stored in positions -3 and -2 of the char* used to reference the std::string.

Source: Nicholas Ormrod

Before delving into how Small String Optimization (SSO) is implemented, it’s important to revisit how regular string objects are defined. Here’s a typical std::string implementation using modern C++:

struct string {
  std::unique_ptr<char[]> data; // data
  std::size_t size; // actual size
  std::size_t capacity; // total capacity of the buffer
}

With this implementation, we have a string class with a size of 24 bytes (on 64-bit architectures). If we compare this to the actual std::string in C++ found in the <string> header, we can observe that the C++ implementation has a size of 32 bytes. This size difference is attributable to Small String Optimization (SSO), which consumes some space within the struct. When SSO is applied, the implementation changes to something similar to this:

struct string_sso {
  std::size_t capacity;
  std::unique_ptr<char[]> data;
  std:size_t size;
  std::array<char, 16> sso_buffer;
}

The magic number 16 is determined by the compiler’s implementation and typically corresponds to 16 (15 characters + 1 null terminator).

This implementation has its advantages and disadvantages. If your string is shorter than 15 characters, it will be stored inside the std::array on the stack. However, if it exceeds 15 characters, your string will occupy 40 bytes in memory, with 24 bytes of that space going unused when SSO (capacity, size and data won’t be necessary until we reach 16 characters).

Note: std::array already has its own size that can be calculated in O(1).

The following implementation offers a more efficient way to utilize memory, paying only for what you actually use.

struct string_sso {
  std::size_t capacity;
  union {
    struct {
      std::unique_ptr<char[]> data;
      std:size_t size;
    } heap_buffer;
    std::array<char, sizeof(heap_buffer)> sso_buffer;
  };
}

In this new implementation, we utilize a static array (std::array) that eliminates the need for memory allocation on the heap. The critical element here is the sizeof(heap_buffer), which on a 64-bit architecture would be 16 bytes. This buffer accommodates 15 bytes for the string content and 1 byte for the null terminator for small strings. With this approach, the string size remains at 24 bytes, and the use of a union ensures that the same space is utilized whether the string size is larger or smaller than 16 bytes.

Personally, I prefer the second implementation, even though it lacks the flexibility to set a fixed size for every architecture or operating system, and it is tightly bound to data sizes. There are two potential reasons for using the first approach in compilers: either they consider it more efficient or they want to maintain compatibility with the existing Application Binary Interface (ABI).

Performance

The answer seems obvious, if our data structure is smaller and non allocated on the heap, our operations will be faster since it is faster to allocate and retrieve from the stack than from the heap. There are a lot of testing doing in the internet about that but I would like to add two of the most clear and well done that I found.

Source: https://cpp-optimizations.netlify.app/small_strings/
Source: HugoTeixeira in https://stackoverflow.com/a/51796541

Specially in the last one we can see clearly how in MacOS the SSO size is set to 23 characters, instead of 15 that is in Ubuntu

FBString by Andrei Alexandrescu

In the CppCon 2016, Nicholas Ormrod mention in his talk “The strange details of std::string at Facebook”, the implementation done by Andrei Alexandrescu of FBString in order to implement a SSO prior to GCC 5.0.

In contrast to some SSO optimizations implemented by compilers, FBString offers an SSO buffer size of 23 characters, which is larger than the 15 characters provided by the vast majority of implementations. But why is this significant? As Nicholas mentioned in the talk, one of the most commonly used strings at Facebook consists of UUIDs with 20 characters. With the majority of compiler implementations, these strings exceed the maximum characters required to fit within the SSO buffer, leading to heap memory allocation. Aside from Facebook case, 15 characters are not enought space in order to parse some of the 64-bit integer values to strings (int64_t, uint64_t, long long)

You can find the FBString optimization here

This is accomplished through the implementation of a three-tiered storage strategy and by collaborating with the memory allocator. Specifically, FBString is designed to identify the use of jemalloc and work in conjunction with it to attain substantial enhancements in both speed and memory utilization.

FBString SSO uses the last byte to represent the spare capacity for the string (remaining caracters).

Source: Nicholas Ormrod

This is quite clever because 0 is equivalent to ‘\0’. Consequently, when the spare capacity is 0, our last character will be the null terminator.

Source: Nicholas Ormrod

With this implementation, we have 3 bits remaining within the 24 available bytes to use as flags. Here’s a quick summary table about how we store tha values based on the number of characters:

Character rangeStorage method
<= 23 charactersAllocated on the stack
24-255 charactersare stored in malloc-allocated memory and copied eagerly.
> 255 charactersare stored in malloc-allocated memory and copied lazily (copy on write).

Copy-on-write (COW) semantics were removed in C++11 due to multithreading problems, but Andrei’s implementation offers multithreading safety.

Nicholas claims that this new implementation resulted in a 1% performance improvement for Facebook, which may seem substantial for a mere std::string optimization. One of the reasons for this improvement is its high optimization for the cache. Although it generates more assembly instructions, it is more cache-friendly, making the implementation faster. It’s essential to consider whether our target platform has cache available when assessing the impact of this optimization.

Note: Clang and MSVC have a similar implementation.

String_view

std::string_view was introduced in C++17. The primary purpose of this data structure is to optimize operations involving read-only strings. As a result, the std::string_view interface closely resembles that of a constant std::string.

A std::string_view is exceptionally lightweight, consisting of just one pointer to data (8 bytes on 64-bit architectures) and the size (8 bytes on 64-bit architectures), for a total of 16 bytes of space. This property makes it extremely efficient to convert a std::string into a std::string_view using the std::basic_string_view(const CharT* s, size_type count) constructor. This operation is highly resource-efficient.

In contrast, transforming a null-terminated string (const char*) into a suitable data structure typically requires iterating through all the data to retrieve the size of the string. This process has a time complexity of O(n), which can be significant when dealing with very large null-terminated strings, although such situations are relatively rare.

It’s crucial to highlight that std::string_view instances are allocated on the stack, eliminating the need for memory heap allocations. This is made possible by their immutability and their role as a “view” of an existing string, rather than a separate data container.

It’s indeed a good practice to consider using std::string_view whenever you are working with data that effectively behaves like a const std::string& or a const char* without the need for a null terminator

Last but not least, std::string_view does not have a null terminator character!!!

References

  1. https://stackoverflow.com/a/10319672
  2. https://stackoverflow.com/questions/10315041/meaning-of-acronym-sso-in-the-context-of-stdstring/51796541#51796541
  3. https://cpp-optimizations.netlify.app/small_strings/
  4. https://pvs-studio.com/en/blog/terms/6658/
  5. CppCon 2016: Nicholas Ormrod “The strange details of std::string at Facebook”: https://www.youtube.com/watch?v=kPR8h4-qZdk
  6. CppCon 2015: Marshall Clow “string_view”: https://www.youtube.com/watch?v=H9gAaNRoon4

4 Effortless Tricks to Improve Your Code with ChatGPT

It is widely acknowledged that ChatGPT has gained significant recognition in the field, so let us proceed directly to the valuable techniques I have discovered through my extensive experience as a programmer, which I diligently apply in my everyday coding endeavors.

1. Generate code for languages I’m not familiar with

It is a prevalent practice in software development projects to utilize multiple programming languages. When working with C++, for instance, it is often necessary to employ additional languages such as CMake, Bash, Python, and others. However, it is quite common to use these auxiliary languages for specific tasks and subsequently overlook them.

This approach proves particularly advantageous for C++ projects, as the initial setup process in CMake can be intricate and time-consuming. In situations where a simple configuration is all that is needed to initiate a project, employing this technique can save valuable time and effort.

ChatGPT output for the question: "Generate a CMake project"

By adopting this approach, I am able to initiate a project promptly without the need to comprehend the intricacies of CMake or its usage. This significantly reduces the time required for project setup, enabling me to dive into coding swiftly.

Another language that has proven immensely valuable is Bash. However, working with Bash sometimes necessitates an extensive understanding of the language itself as well as the Linux operating system. This dependency can be mitigated by leveraging the capabilities of ChatGPT. For instance, consider the following example where I require a quick method to obtain a list of all process IDs (PIDs) that are currently running, and subsequently remove a specific process by providing an input parameter.

2. Explain code

In the software development realm, it is not uncommon to encounter scenarios where we need to delve into pre-existing legacy code, whether they were generated for us or by other individuals. As time goes by, it becomes increasingly challenging to comprehend code that we ourselves wrote in the distant past. This difficulty amplifies when dealing with external code. However, ChatGPT excels in its ability to provide comprehensive explanations of code. Let’s consider the following code snippet:

int randomNumber = Random::get<int>(0, std::numeric_limits<int>::max());
// current date/time based on current system
std::time_t now = std::time(0);

// convert now to string form
std::string time = std::asctime(std::localtime(&now));
std::string title("Simulations/Simulation" + std::to_string(randomNumber) +".csv");
std::ofstream stream(title);
csv2::Writer<csv2::delimiter<','>> writer(stream);

This is the depth explanation:

3. Comment code

Generating meaningful and concise comments for code can often be a tedious and challenging task. It requires careful consideration of how other programmers who read the code will comprehend it accurately. Thankfully, ChatGPT can assist in automating this process. Let’s continue with the previous code snippet and ask ChatGPT to generate meaningful comments for it:

// Generate a random number
int randomNumber = Random::get<int>(0, std::numeric_limits<int>::max());

// Get the current system time
std::time_t now = std::time(0);

// Convert the time to a string
std::string time = std::asctime(std::localtime(&now));

// Create the file name for writing the CSV data
std::string title("Simulations/Simulation" + std::to_string(randomNumber) + ".csv");

// Open the file for writing
std::ofstream stream(title);

// Create a CSV writer for the file
csv2::Writer<csv2::delimiter<','>> writer(stream);

4. Improve code

Setting personal ego aside, it is a common occurrence for programmers to find themselves needing to make quick implementations to meet deadlines or due to the desire for efficiency. In such situations, ChatGPT proves to be highly valuable as it excels in improving code in ways that may not have been initially considered by the programmer.

The following code snippet is extracted from a personal project TwitchLootClicker that it is a Google Chrome extension in Javascript. Javascript is an easy language to pick up quickly but I am not very familiar with so I tend to code like if I still using C++:

function millisToMinutesAndSeconds(millis) {
    if(millis < 0){
        return "Ready to claim";
    }
    var minutes = Math.floor(millis / 60000);
    var seconds = ((millis % 60000) / 1000).toFixed(0);
    return minutes + ":" + (seconds < 10 ? '0' : '') + seconds;
}

function millisToHoursMinutesAndSeconds(millis){
    var seconds = (millis / 1000).toFixed(0);
    var minutes = Math.floor(seconds / 60);
    var hours = "";
    if (minutes > 59) {
        hours = Math.floor(minutes / 60);
        hours = (hours >= 10) ? hours : "0" + hours;
        minutes = minutes - (hours * 60);
        minutes = (minutes >= 10) ? minutes : "0" + minutes;
    }

    seconds = Math.floor(seconds % 60);
    seconds = (seconds >= 10) ? seconds : "0" + seconds;
    if (hours != "") {
        return hours + "h " + minutes + "m " + seconds + "s";
    }
    return minutes + "m " + seconds + "s ";
}

function minutesToMilliseconds(minutes){
    return (minutes * 60000);
}

The first function serves to convert milliseconds to the format of minutes and seconds (MM:SS). The second function converts milliseconds to the format of hours, minutes, and seconds (HH:MM:SS). Lastly, the third function converts minutes to milliseconds.

Introducing the next prompt to ChatGPT: “Refactor this code using idiomatic JavaScript conventions.” The goal of this prompt is to modify the code to adhere to the standard programming practices followed by JavaScript developers.

/**
 * Converts milliseconds to minutes and seconds
 * @param {number} millis - The time value in milliseconds
 * @returns {string} The formatted time in "minutes:seconds" format
 */
function millisToMinutesAndSeconds(millis) {
  if (millis < 0) {
    return "Ready to claim";
  }

  const minutes = Math.floor(millis / 60000);
  const seconds = Math.floor((millis % 60000) / 1000).toString().padStart(2, '0');
  return `${minutes}:${seconds}`;
}

/**
 * Converts milliseconds to hours, minutes, and seconds
 * @param {number} millis - The time value in milliseconds
 * @returns {string} The formatted time in "hours:h minutes:m seconds:s" format
 */
function millisToHoursMinutesAndSeconds(millis) {
  const seconds = Math.floor(millis / 1000);
  const minutes = Math.floor(seconds / 60);
  let hours = "";

  if (minutes > 59) {
    hours = Math.floor(minutes / 60).toString().padStart(2, '0');
    minutes %= 60;
  }

  const formattedMinutes = minutes.toString().padStart(2, '0');
  const formattedSeconds = (seconds % 60).toString().padStart(2, '0');

  if (hours !== "") {
    return `${hours}h ${formattedMinutes}m ${formattedSeconds}s`;
  }

  return `${formattedMinutes}m ${formattedSeconds}s`;
}

/**
 * Convert minutes to milliseconds
 * @param {number} minutes - The time value in minutes
 * @returns {number} The equivalent time value in milliseconds
 */
function minutesToMilliseconds(minutes) {
  return minutes * 60000;
}

Those are the improvements done by ChatGPT to the former code:

  • Added type annotations to the function parameters for better code documentation.
  • Used template literals (“) for string concatenation, making the code more readable.
  • Replaced the toFixed() method with Math.floor() and toString().padStart() to ensure consistent formatting and avoid floating-point errors.
  • Used the const keyword for variables that don’t require reassignment.
  • Made the code more concise and eliminated unnecessary variable assignments.

What are Git Hooks and how to use them in your Rust projects

What is a Git Hook?

Git hooks are script files that can be executed in response to specific events or operations. They can be classified into two distinct categories: client-side hooks and server-side hooks.

How to create a Git Hook

Git hooks are located within the .git/hooks directory. During the initialization of a repository using git init, a set of example hooks is generated, covering various types of hooks. These examples are suffixed with .sample, and in order to utilize them, the suffix must be removed. The provided examples are predominantly written as shell scripts, incorporating some Perl, but any appropriately named executable scripts can be used—be it Ruby, Python, or any other language you are familiar with.

Each hook possesses its own unique interpretation, necessitating a review of the provided examples or documentation to determine whether specific parameters are required, if a return value is expected, and other relevant specifications.

List of Git Hooks

  • applypatch.msg: Check the commit log taken by applypatch from an e-mail message. This script can modify the commit message. Exit with a non-zero status to stop the commit.
  • post-update: Prepare a packed repository for use over dump transports.
  • pre-applypatch: This script is executed before the applypatch hook.
  • prepare-commit-msg: This hook is called with git commit and it is used to prepare the commit message. It receives two parameters: the name of the file that has the changes and the description of the commit. If the script returns a non-zero status, the commit will be aborted
  • commit-msg: Called with one argument, the name of the file that has the commit message. This script can modify the commit message. Exit with a non-zero status to stop the commit.
  • pre-commit: This hooks is executed when you use git commit command. It takes no arguments but it is useful to pause the commit returning a non-zero status with a message. E.g: stop the commit if the code is not properly formatted or stop the commit if tests are failing.
  • post-commit: It runs once the git commit is finished. It does not take any argument and it is usually used to provide custom messages.
  • pre-merge-commit: This hook is executed when you are about to perform a git merge operation. It takes no arguments and should return a non-zero status to stop a merge.
  • pre-push: This hooks is executed when you perform a git push command. After checking the remote status, but before pushing anything. It takes two arguments: name of the remote branch and the URL to which the push will be done. If this script retun a non-zero status, it will stop the push.
  • pre-rebase: This hooks is executed before git rebase is started. It takes two arguments: the upstream the series was forked from and the branch being rebased (or empty when rebasing the current branch). If it returns a non-zero status, the rebase will be aborted.
  • post-checkout: Executed after a successful git checkout.

Check the complete list in the next url: Official list with all the git hooks

Setup Git Hooks with Rust

A wide range of freely available crates exists to enhance the implementation of git hooks in Rust projects. Personally, I have chosen to utilize the rusty-hook crate.

There are two methods for incorporating this crate into our project. The first approach involves adding it as a dev dependency in our Cargo.toml file. Upon building the project for the first time, it will be automatically initialized:

TOML
[dev-dependencies]
rusty-hook = "^0.11.2"

Or install and initialize it:

Bash
cargo install rusty-hook
rusty-hook init

How to use it

Once the crate is initialized we will be able to see new file named .rusty-hook.toml at the root of our project. This file is the only thing we need to work with rusty-hook.

The first time we open the file everything is already set up.

TOML
[hooks]
pre-commit = "cargo test"

[logging]
verbose = true

Under the [hooks] section, we have the ability to assign a custom script that will be executed for a specific git hook. The following example, extracted from a personal project, showcases a script that formats the code, runs clippy (a Rust linter), and executes all tests in the project before committing the staged changes.

TOML
[hooks]
pre-commit = "cargo fmt && git add . && cargo check && cargo test && cargo test --workspace -- --include-ignored --show-output && cargo clippy"
pre-push = "cargo fmt -- --check || cargo fmt && cargo fmt -- --check"

References:

Swap_remove in Rust

Introduction

The other day, looking at the std::Vec documentation in Rust, I found this interesting function called swap_remove.

First, we have to talk about the obvious difference between the standard remove, that everybody has used before, and this swap_remove that is a more tricky implementation for some use cases:

Remove

Remove, as defined in its documentation, follows the next function signature: pub fn remove(&mut self, index: usize) -> T. It recieves the mutable vector itself, and the desired index of the element to be removed, and return it.

What it is important to mention is how Rust (and almost every other language) handle vectors. Vectors in computer science are defined as a collection of elements followed in memory, that means, the distance between two elements of a vector is the size of the element.

4 blocks with the values 1,2,3,4, splited in 4 bytes each since they are signed integers of 4 bytes

Once we delete an element that is in the middle of the structure (if you want to delete the first or last element Rust recommends to use VecDeque instead), Rust has to shift the elements to avoid empty spots in the collection. This work has a O(n) complexity in the worst case. Wich means that if we have one million elements and we want to remove the second one, the vector will have to shift 999.998 elements!!!

Swap_remove

The function swap_remove is useful to handle this problem. This operation will swap the last element with the one to be erased, and finally perform a back pop to remove it. E.g: If we remove the value 2, the vector will look like the following:

4 blocks with the values 1,4,3, splited in 4 bytes each since they are signed integers of 4 bytes
Since access and insertions in a vector are O(1), this provides vec::swap_remove a complexity of O(1). Wich means that we have to make 999.997 less operations than the remove method. Cool right?

When to use it or not

With the last explanation seems like we have to use always swap_remove instead of remove right? Thats wrong!!!

Vectors are a powerful structure when we have the values sorted. This invalidates the whole point of using swap_remove, since we will have to sort the values again.

Rust
let mut v: Vec<i32> = vec![1, 2, 3, 4];
assert!(v.is_sorted());
v.swap_remove(1); // v = [1, 4, 3]
assert!(!v.is_sorted());

We have to be careful with working with raw pointers in Rust since they are unsafe, but I will explain some errors that we can find.

Rust
let mut v: Vec<i32> = vec![1 ,2, 3, 4];
let points_to_four = &v[3] as *const i32;
let points_to_two = &v[1] as *const i32;
assert_eq!(unsafe{*points_to_four}, 4);
assert_eq!(unsafe{*points_to_two}, 2);
v.swap_remove(1);
assert_eq!(unsafe{*points_to_four}, 4); // Could be false at some point that we can not control
assert_ne!(unsafe{*points_to_two}, 2); // Now points to the value 4 at v[1]

This test was very interesting. It seems like Rust clones the elements to swap instead of moving it, this is understandable since an i32 implements the Copy and Clone traits.

Those are unsafe operations but we have to be very careful of what are we doing because if our Vec<T> where T !Copy, the assert could be or not true. By the way, even if now it is true because the value is copied, this could change in the future once the OS claim this free memory address. This happens aswell with vec::remove.

Rust
let mut v: Vec<i32> = vec![1, 2, 3, 4];
let last_ptr = &v[3] as *const i32;
assert_eq!(unsafe{last_ptr.read()}, 4);
v.swap_remove(1);
assert_eq!(unsafe{last_ptr.read()}, 4); // Still true
v.insert(1, 10);
assert_eq!(unsafe{last_ptr.read()}, 4); // FAIL: Left: 3, Right: 4. The vector has growth, shifting the value 3 to the right.

Conclusion

We should use vec::remove when we care about the order and vec::swap_remove otherwise. Working with raw pointers and remove elements of a vector is highly unsafe, but at worst… We have the same as C++ right?

The reason you should test your personal projects

Why do we underestimate testing?

Testing in software development tend to be the forgotten and underestimated part. It usually takes time and do not produce real products or concrete features, this is why it usually get relegated in order to prioritize other aspects in a development cycle.

There is a rule that says you have to have one line of testing for one line of code. In practice, this is usually not fulfilled, usually due to time, money and resources. Companies ofter underestimate how powerfull testing is and spend a lot of hours in finding and solving bugs that shouldn’t even been in production.

Testing in big projects

Testing in big projects is always a must. Even thought, you have more or less tests, better or worse tests, but there are always some testing in huge projects where a lot of people are involved and one minor change can downfall into a bug issue. Companies are aware of that and tend to force their developers to implement testing in the code, but let’s be honest, testing is usually the part that developers hate to implement and they tend to get rid of it as soon as possible, letting a lot of cases uncovered.

Testing in small and personal projects

But obviously, if there is a place when testing is absolutely forgotten is in small and mainly personal projects. Ey! I do not say that you have to “waste” time testing you small project used by absolutely nobody in the world. But this trend of not implement testing in small projects tend to extend to libraries or small software products used by users.

Almost 6 months ago, I started learning Rust and trying to heavily participate in adding my small grain of sand to the ecosystem the comunity is building arround it. That includes libraries, frameworks, external tools and also projects.

When you add a library made by someone in your project, you are trusting this piece of software. You take for granted that eveything should work as intended. And you, as developer, are in the moral responsability that this is true, otherwise, you are not only developing a bad software but making other developers to waste their time by your fault.

Hidden gems in testing

Programmers usually understand testing as a way to ensure that his code is well constructed for any possible scenarios that may happen in the execution.

Meanwhile this is true, it is only one of the reasons for testing. A part for covering all the possible edge cases that your program can have, there are other things that testing hold your back.

Development speed in the future

Adding test to your software gives you the ability to quickly ensure that any modification you are making will not break the existing logic. This is the main reason testing is done in big projects. You want to implement a new feature or fix a bug and if testing is properly done, you do not have to mind if you are breaking other stuffs with your new implementation because the tests will ensure that for you.

Third party libraries

If we only focus on us as the only people that can break our software we are wrong. Usually, in a product development we use a bunch of third party libraries that are constantly updating and changing its logic. Maybe that third party library that you used for a function called getDollars(float euros) have change in its last release and now its new implementation is more like this: getDollars(float pounds).

This change in a third party library will produce a silent bug in our code. Now, all of our conversions will return an incorrect value since we thought we are converting our euros to dollars. But the program will not crash in any way possible unless we made the propers tests for that.

External changes

Sometimes, our software depends in some public data or values that we take for granted as inmutable. This is the parciular case that made me make this post.

Recently, I have been working in a Rust project that I will talk more about it soon. The project consist in web scrapping some website in order to retrieve information and process it. For this reason, the implementation of my logic heavily depends on how the web page structure is done and any minor change in the website made by their developers can easily break all my code.

Obviously, I can not anticipate on how and when the web programmers and UI/UX teams will update the site. In this case, aswell as tests that tries to cover all the possibles fails if the page layout changes, I also want that my tests fails if someday this happens. For example:

HTML
<!-- Version 1.0 -->
<html>
	<body>
        <p> Valuable data $$ </p>
    </body>
</html>
C++
// logic.cpp
std::string data = get_first_element_in_body();

In this case our logic is simple, we want to get the first element inside the body in order to retrieve our valuable data. But then, the webpage made a change and this is the new code:

HTML
<!-- Version 1.1 -->
<html>
	<body>
        <h1> Stupid title </h1>
        <p> Valuable data $$ </p>
    </body>
</html>

With this small change, all our code is absolutely and silently broken. The developers decided that they wanted to add a title before that information for stetics reasons and now our webscrapping does not provide the same result as before. This would be so difficult to find without tests for that. Imagine we are storing this directly in a database or sending it to another resource. We wouldn’t know anything until the user tell us.

This can be prevented with a simple small test

C++
// test.cpp
std::string data = get_first_element_in_body();
static_assert(is_this_data_valuable(data));

Conclusion

Testing is as important as real product implementations. We have to start thinking as live saver and not as time wasted. In this article, I also proved that testing is not only important for breaking changes that you may introduce in your code but also for silent bugs introducen not even by yourself. I hope after this read you start seeing testing this way. You can not imagine how many stress take away from you when developing to know that your product is working as intended just by running a single command that executes the tests or even a CI flow that ensure that nobody can change anything that breaks the existing implementation.

Memory alignment and layout in Rust

Introduction

When working with data values it is not arbitrary the way we align those values. At first glance we may think that we should just store one next to the other no matter what. This is often true but not always, in this article I am going to explain different ways we have in Rust to explicitly define how we want our structures to be aligned.

Layout alignment in Rust

The Rust compiler does not guarantee to generate the same layout alignment every compilation. What that means is that our structures of data may be different everytime we change the code. Sometimes this could be good (obviously there is a reason why Rust does that) but sometimes could tend to performance issues we do not want. Is always a fight between two properties: Size vs Speed.

Size vs Speed

This is the main reason to choose one or another layout representation. Sometimes we are struggling with low capacity microcontrollers and we need this boost of space optimization to store our structures. In this kind of situations is where it is better to just represent our data structures layout as sequencials as possible.

On the other hand, other times we face optimization issues where our code is not fast enought to perform the task we need. The cache and the way we access to our memory is the key to have a well organized data layout.

We are not going to discuss how cache and memory access works in computers but you can find more about this topic in the following links:

Default representation

With the attribute #[repr] we can specify the way we want to represent our values in the layout. If no attribute value is presented, Rust will use its own representation that does not guarantee any kind of data layout.

C representation

The #[repr(C)] attribute lets us represent our data structures the way C does it. This way let Rust to interoperate with C. Representation in Rust with the C way has some defined properties:

  • Structure alignment: The alignment of the struct is the alignment of the most-aligned field in it, e.g: If we have a struct called T with two values; bool and u32. The structure alignment would be 4 bytes (based on u32).
  • Size and offset: For each field in declaration order in the struct, first determine the size and alignment of the field. If the current offset is not a multiple of the field’s alignment, then add padding bytes to the current offset until it is a multiple of the field’s alignment. The offset for the field is what the current offset is now. Then increase the current offset by the size of the field. Finally, the size of the struct is the current offset rounded up to the nearest multiple of the struct’s alignment.

Pseudocode:

Rust
/// Returns the amount of padding needed after `offset` to ensure that the
/// following address will be aligned to `alignment`.
fn padding_needed_for(offset: usize, alignment: usize) -> usize {
    let misalignment = offset % alignment;
    if misalignment > 0 {
        // round up to next multiple of `alignment`
        alignment - misalignment
    } else {
        // already a multiple of `alignment`
        0
    }
}

struct.alignment = struct.fields().map(|field| field.alignment).max();

let current_offset = 0;

for field in struct.fields_in_declaration_order() {
    // Increase the current offset so that it's a multiple of the alignment
    // of this field. For the first field, this will always be zero.
    // The skipped bytes are called padding bytes.
    current_offset += padding_needed_for(current_offset, field.alignment);

    struct[field].offset = current_offset;

    current_offset += field.size;
}

struct.size = current_offset + padding_needed_for(current_offset, struct.alignment);

Simple and small example extracted from allaboutcircuits:

Rust
#[repr(C)]
struct T {
    c: u32,
    d: bool,
}
Image from: https://www.allaboutcircuits.com/technical-articles/understanding-memory-structures-in-embedded-c-language/

#[repr(C)] for unions and enums

Unions

The #[repr(C)] for unions types will be structured with size of the maximum value in the union rounded with the alignment, and an alignment of the maximum alignment of all of its fields.

Rust
#[repr(C)]
union MyUnion {
    f1: u64,
    f2: [u32; 8],
}

fn main() {
    assert_eq!(std::mem::size_of::<MyUnion>(), std::mem::size_of::<u32>() * 8);
    assert_eq!(std::mem::align_of::<MyUnion>(), std::mem::size_of::<u64>());
}

Enums with fields

The representation of an enum with fields is a struct with two fields, also called a “tagged union” in C:

Rust
// This Enum has the same representation as ...
#[repr(C)]
enum MyEnum {
    A(u32),
    B(f32, u64),
}

// ... this struct.
#[repr(C)]
struct MyEnumRepr {
    tag: MyEnumDiscriminant,
    payload: MyEnumFields,
}

// This is the discriminant enum.
#[repr(C)]
enum MyEnumDiscriminant { A, B, C, D }

// This is the variant union.
#[repr(C)]
union MyEnumFields {
    A: MyAFields,
    B: MyBFields,
}

#[repr(C)]
#[derive(Copy, Clone)]
struct MyAFields(u32);

#[repr(C)]
#[derive(Copy, Clone)]
struct MyBFields(f32, u64);

fn main() {
    assert_eq!(std::mem::size_of::<MyEnum>(), std::mem::size_of::<MyEnumRepr>());
}

References

  1. https://doc.rust-lang.org/reference/type-layout.html
  2. Rust for Rustaceans: Idiomatic Programming for Experienced Developers (B0957SWKBS)

Custom conversions in C++

Introduction

In some languages it is common to have conversions between different types such as converting an integer to a floating point, for example. There are two types of conversions in programming; implicit conversions and explicit conversions.

Explicit conversions

An explicit conversions, as the name suggest, it is when the programmer have to explicitly specify what conversion has to be done. This way the code may look more messy but the programmer has the control of the code in all time.

C++
void floatToInt(int x) {
    std::cout << x; // prints 42
}

int main() {
    float x = 42.51231;
    floatToInt(static_cast<int>(x));
}

Implicit conversions

Implicit conversions are a powerfull tool that some languages provide usually to make the code easier to read and more agile when coding. Implicit conversions as the name suggest are done behind the scenes, wich means that the language itself knows that one type has to be converted to another type. In the code snippet below we can see an example:

C++
void floatToInt(int x) {
    std::cout << x; // prints 42
}

int main() {
    float x = 42.51231;
    floatToInt(x);
}

In this case, we can see how easily is to lose information in some data types if you rely in implicit conversion too much. This is te reason some modern languages like Rust doesn’t allow implicit conversions.

“explicit” specifier

explicit specifier is a keyword that C++ provides and can be used in constructors and conversion functions to avoid undisered implicit conversions.

C++
struct X {
    X(int) {}
    operator std::string() const { return std::string();}
};

struct Y {
    explicit Y(int) {}
    explicit operator std::string() const {return std::string();}
};
 
int main() {
    X x1 = 10;
    X x2(10);
    X x3 = {10};
    std::string s1 = x1;
    std::string s2 = static_cast<std::string>(x1);

    Y y1 = 10; // Error: cannot convert int to Y
    Y y2(10);
    Y y3 = {10}; // Error: cannot convert initializer-list to Y
    std::string s3 = y2; // Error: Implicit conversion not implemented.
    std::string s4 = static_cast<std::string>(y2);
}

In an effort for C++ to prevent issues with implicit conversion we have the ‘uniform initialization’ or ‘braced initialization’ in C++ 11 with the operator{}. This operator forze us to expecify the exact type our constructor is expecting.

C++
struct Z {
    Z(int, bool) {}
};

int main() {
    int x1(10.5); // Implicit conversion from double to int -> 10
    int x2{10.5}; // Error: narrowing conversion from double to int.
    
    Z z1(10.5, -1);
    Z z2{10, -1}; // Error: narrowing conversion int to bool.
    Z z3{10, false};
}

Explicit functions

But since ‘braced initialization’ only applies when constructing a type or an object, if we want a specific function to only accept the type we are indicating, the solution is a little bit more tricky.

1. Deleting function overloads
C++
struct Z {
    Z() = default;
    void Foo(int) {}
    void Foo(float) = delete;
    void Foo(bool) = delete;
};

int main() {
    Z z1;
    z1.Foo(1);
    z1.Foo(1.5); // Error: use of deleted function
    z1.Foo(true); // Error: use of deleted function
}

We can use generic parametrization to ensure that only functions we declare are going to be called.

C++
struct Z {
    Z() = default;
    void Foo(int) {}
    
    template<typename T>
    void Foo(T) = delete;
};

int main() {
    Z z1;
    z1.Foo(1);
    z1.Foo(true); // Error: use of deleted function
    z1.Foo(1.5); // Error: use of deleted function
    z1.Foo('a'); // Error: use of deleted function
}
2. Concepts

In C++ 20 concepts were added and it is the proper way to address this problem. Let’s see an example:

C++
template<typename T> 
    requires std::same_as<T, int>
void Foo(T) {}

But we can do it in a shorter way using auto.

C++
concept is_integer = std::same_as<T, int>;

void Foo(is_integer auto) {}

User defined conversions in C++

C++ is one of those languages that give us tools to play with almos every aspect of an implementation. Usually, programming languages with type conversions have some conversiones already defined by the standard. In this case, C++ allow us to define our owns conversions. In the next code snippet we can see an explicit and implicit conversion between two custom types. Both structs recieve 2 std::strings& and 1 int and implement custom casting to std::string.

C++
struct FullNameExplicit {
    explicit FullNameExplicit(const std::string& name, const std::string& second_name, int age) :
    name(name),
    second_name(second_name),
    age(age) {}

    explicit operator std::string() const {
        return name + ' ' + second_name + " has " + std::to_string(age) + '\n';
    }

    std::string name;
    std::string second_name;
    int age;
};

struct FullNameImplicit {
    FullNameImplicit(const std::string& name, const std::string& second_name, int age) :
    name(name),
    second_name(second_name),
    age(age) {}

    operator std::string() const {
        return name + ' ' + second_name + " has " + std::to_string(age) + '\n';
    }

    std::string name;
    std::string second_name;
    int age;
};

void Foo(const std::string person) {
    std::cout << person;
}

int main() {
    FullNameExplicit fne("Ruben", "Rubio", 24);
    Foo(fne); // Error: implicit conversion not defined.
    Foo(static_cast<std::string>(fne));
    
    FullNameImplicit fni("Ruben", "Rubio", 24);
    Foo(fni);
    Foo(static_cast<std::string>(fni));
}

Conclusion

In my humild opinion, I think we should avoid implicit conversion as much as possible. Particularly, when working with other people involved in the code they may not be aware of the implicit conversions you are using and this can tend to get lost easily in a large project.

References

  1. https://en.cppreference.com/w/cpp/language/cast_operator
  2. https://en.cppreference.com/w/cpp/language/explicit
  3. https://stackoverflow.com/questions/121162/what-does-the-explicit-keyword-mean

ADL (Argument-dependent lookup) explained

ADL also known as argument-dependent lookup or Koenig lookup (even thought he said that he have not discovered it), is a very old and unfamiliar C++ feature.

ADL work is to set a couple of rules to lookup for functions based on their namespaces parameters provided, e.g:

C++
int main() {
    std::vector<int> v;
    
    auto it = std::begin(v); // Common std::begin function.
    auto itAdl = begin(v); // Also calling std::begin because 'v' is in the std namespace.
}

This is a common piece of code in C++ that everyone has written without knowing why exactly works. In this case both ways are equivalent because they are calling std::begin. The first one is called directly and the second one is found thanks to ADL set of rules.

This is very powerfull specially when working with operators like the example below:

C++
int main() {
    const std::string& out = std::string("Hello") + "world";
    std::cout << out;
}

There is neither a + or << global operator but thanks to ADL it is smart enought to look for them in their relative namespaces. Let’s take a look what are the real code generated by the compiler using https://cppinsights.io/

C++
int main() {
  const std::basic_string<char, std::char_traits<char>, std::allocator<char> > & out = std::operator+(std::basic_string<char, std::char_traits<char>, std::allocator<char> >(std::basic_string<char, std::char_traits<char>, std::allocator<char> >("Hello", std::allocator<char>())), "world");
  std::operator<<(std::cout, out);
}

Both operators have been converted to std::operator calls.

Another very cool feature is to prioritize our own function calls when overriding some std implementations:

C++
template<typename T>
int accumulate(T begin, T end, int sum) {
    return 42;
}

int main() {
    std::vector<int> values{10,30,10};
    int sum{0};
    accumulate(values.begin(), values.end(), sum); // Returns 42.
}

In this case if our own accumulate implementation have not been provided, std::accumulate would be called instead.

Obviously we can take advantage of this an use it in our own code. Even thougt I personally think it is a better practice to always specify the namespace on our functions and objects, it could be a very powerfull solution if at some time we want to change a large piece of code just by swapping namespaces.

C++
namespace ns1 {
    struct X{};
    void g(const X x) {}
    void f(const X x) {}
}

namespace ns2 {
    struct X{};
    void g(const X x) {}
    void f(const X x) {}
}

int main() {
    ns1::X x;
    g(x); // ns1::g(ns1::X) is called.
    f(x); // ns1::f(ns1::X) is called.
}

In this particular example if we change the namespace in the object declaration, all the function calls would also change.

References

  1. https://en.cppreference.com/w/cpp/language/adl
  2. https://www.drdobbs.com/cpp/a-personal-note-about-argument-dependent/232901443
  3. https://www.youtube.com/watch?v=agS-h_eaLj8&ab_channel=C%E1%90%A9%E1%90%A9WeeklyWithJasonTurner
  4. https://stackoverflow.com/questions/8111677/what-is-argument-dependent-lookup-aka-adl-or-koenig-lookup

return false vs return true

Introduction

You might think that this is a stupid debate, and maybe you are right but let me take a moment to explain to you if we should care whether to return true or false.

True and false are the basis in computer programming and traditional logic. The binary system is used based on this only two possibilities, the value is either one or zero.

To understand if returning true or false has an impact in our code performance we have to talk about how computers work in a very low level, in specific I’m going to talk a bit about x86-64 assembly and different C++ compilers.

Benchmarking

Once we compile our code in C++ the compilers convert the code into assembly code. In assembly we usually talk about number of instructions instead of number of lines. And even different instructions can take different number of cycles to be processed in our CPU. Okey, let’s see how return true and return false is compiled into assembly code using GCC 11.2 and Clang 12.0.1. Both without any optimizations on.

Clang 12.0.1 -O0
C++
/*
    push    rbp 
    mov     rbp, rsp
    xor     eax, eax
    and     al, 1
    movzx   eax, al
    pop     rbp
    ret
*/
bool ReturnFalse() {
    return false;
}

/*
    push    rbp
    mov     rbp, rsp
    mov     al, 1
    and     al, 1
    movzx   eax, al
    pop     rbp
    ret
*/
bool ReturnTrue(){
    return true;
}
GCC 11.2 -O0
C++
/*
    push    rbp
    mov     rbp, rsp
    mov     eax, 0
    pop     rbp
    ret
*/
bool ReturnFalse() {
    return false;
}

/*
    push    rbp
    mov     rbp, rsp
    mov     eax, 1
    pop     rbp
    ret
*/
bool ReturnTrue(){
    return true;
}

As we can see Clang takes 4 more instructions than GCC to do the same piece of code. Now let’s take a look what happens when we turn optimizations on (-O3) in both compilers.

Clang 12.0.1 -O3
C++
/*
    xor     eax, eax
    ret
*/
bool ReturnFalse() {
    return false;
}

/*
    mov     al, 1
    ret
*/
bool ReturnTrue(){
    return true;
}
GCC 11.2 -O3
C++
/*
    xor     eax, eax
    ret
*/
bool ReturnFalse() {
    return false;
}

/*
    mov     eax, 1
    ret
*/
bool ReturnTrue(){
    return true;
}

Now both compilers are able to perform both tasks in only 2 instructions. But as I mention before all instructions are not the same, let’s take a moment to analize this in machine code.

The instruction mov definitions is: “Copies the second operand (source operand) to the first operand (destination operand)”. Wich means that we are copying the right value to the left register. And its translation to machine code is:

ShellScript
mov eax, 1 # b8 01 00 00 00
ShellScript
mov al, 1  # b0 01

Why are the machine code different if both instructions perform the same operation? This is because copying a value into a register depends on the register. In this case eax is a 32 bits register meanwhile al is an 8 bit subset of eax register.

x86-64 register explanation by: https://www.tutorialspoint.com/assembly_programming/assembly_registers.htm

On the other hand, the xor definition says: “Performs a bitwise exclusive OR (XOR) operation on the destination (first) and source (second) operands and stores the result in the destination operand location“. A classic XOR logic operation which has the advantage that if done with the register itself it will always become 0. The CPU is extremely optimized to perform this kind of bitwise operations.

XOR gate output by: https://sites.google.com/a/cecytebc.edu.mx/audg_2014-ii_tics/mecatronica/2bmt-digitales

Conclusion

Returning 0 (false) seems to be better than returning 1 (true) but the difference it is almost indistinguishable. Even thought it is always a good practice to analize your problem and try to optimize it as much as you can, e.g: You can not always return false instead of true, what you can is to study the percentage of times your result will be true or false. This is a common practice when developing in assembly code specially when you have to deal with some if-else and you really want to just ride of the evaluations as fast as possible.

References

  1. https://randomascii.wordpress.com/2012/12/29/the-surprising-subtleties-of-zeroing-a-register/
  2. https://qastack.mx/programming/33666617/what-is-the-best-way-to-set-a-register-to-zero-in-x86-assembly-xor-mov-or-and
  3. https://godbolt.org/z/reTEPE3En