Click here to Skip to main content
15,113,692 members
Articles / Programming Languages / C#
Tip/Trick
Posted 28 Aug 2021

Tagged as

Stats

7.2K views
4 bookmarked

How to Design a Robust C# Cloud Id in the New Nullable Context with Generics?

Rate me:
Please Sign up or sign in to vote.
0.00/5 (No votes)
4 Sep 2021CPOL11 min read
An overview of nulls and generics with id designs.
In this article, you will get an overview of nulls and generics with id designs, just so you can love the new C# again.

Introduction

The new nullable context can be enabled via the <Project><PropertyGroup><Nullable>enable</Nullable> element in your C# project (.csproj) file. It gives you full null-state static analysis at compile-time and promises to eliminate every single NullReferenceException once and for all. Previously, we talked about the most fundamental null, type and reference checks (How to Start a Robust C# Project in the New Nullable Context with Generics?), as well as the more challenging "index" or range checks (How to Implement a Robust C# Comparison in the New Nullable Context with Generics?), and even the dreaded IDisposable and IAsyncDisposable with a highly practical example (How to Implement a Robust C# IAsyncDisposable in the New Nullable Context with Generics?). Today, we want to dive into the bits with the long promised Id value type that addresses cloud objects. Code quality is of paramount importance and deserves your second look. This article aims to provide an overview of nulls and generics with id designs, just so you can love the new C# again. Little things matter.

Using the code

An id is just an address, ideally having the size of a register for optimal performance. In fact, 64-bit is more than wide enough, as that will address over 18 quintillion different cloud objects, namely, 18 with 18 trailing zeroes. Why not? Unfortunately, we soon will have trillions of devices generating ids globally and independently, independently because it's virtually impossible to coordinate among them to guarantee id uniqueness every time we want a new id. Wait, let me take that back. We actually can, but the cost of deploying such an infrastructure easily overruns billions of US dollars. It might be worth the money, but then again, the network I/O latency associated with each id generation may not be affordable. We desperately need a local solution, one that doesn't require a global infrastructure. UUID was born, an acronym for Universally Unique Identifier! We don't really want a 128-bit id. Nevertheless, UUID is already used in database replication services, whenever a table merge is needed, especially important with multi-master scenarios. UUID is not just an Internet standard. It's in reality used almost everywhere where persistence is a requirement. We can see the entire cloud as a global persistence medium, one where data will always be stored and tracked. Everything known to the Internet is instantly known by the planet, only to be remembered forever. You can't take it back!

Fine, we can live with a 128-bit id, however unlean it is.

A UUID comes in 5 versions, so far. To distingush our own ids from others, we must give it a version. Version-0 is available, but I don't like it. It sounds too humble, immature and unready for the world. I want version-15, the highest possible version in UUID, to demonstrate the perfection of my brilliant design, impossible to surpass, not even by the most intelligent human minds. That's it. It's non-negotiable, hence the following bit pattern, with each question mark to represent a bit position:

???? ????, ???? ????; ???? ????, ???? ????
???? ????, ???? ????; 1111 ????, ???? ????
???? ????, ???? ????; ???? ????, ???? ????
???? ????, ???? ????; ???? ????, ???? ????

Version-15 is 0xf in hex and 0b1111 in bits. Now, we have 124 bits available to us only. Unfortunately, UUID overhead isn't over. We must give it a variant as well. Immediately, we have the following:

???? ????, ???? ????; ???? ????, ???? ????
???? ????, ???? ????; 1111 ????, ???? ????
10?? ????, ???? ????; ???? ????, ???? ????
???? ????, ???? ????; ???? ????, ???? ????

So, we have a mere 122-bit id at our disposal, thanks to UUID versioning and variance. In the computing world, everything is a number, no more and no less. The most primitive form of our version-15 UUID is a guadruple-precision floating-point, large enough to cover each single .Net primitive data type, including Guid and decimal. This is exciting, because every .Net object has now a persistent and permanent id, which we can easily communicate over the cloud. The bit pattern below specifies a standard IEEE guadruple-precision floating-point in UUID, where + indicates a sign bit, e an exponent bit and s a significand bit, with 7 bits stolen:

+eee eeee, eeee eeee; ssss ssss, ssss ssss
ssss ssss, ssss ssss; 1111 ssss, ssss ssss
100s ssss, ssss ssss; ssss ssss, ssss ssss
ssss ssss, ssss ssss; ssss ssss, ssss ssss

Why 7 bits stolen instead of 6 bits used by UUID versioning and variance? Well, we need an extra bit in 0 to mark that it's primitive. Otherwise, it's a composite object. Somehow, it turns out that we magically end up with just enough bits to encode all primitive data types in .Net, no more and no less. We want to do better than that. As we all know, a decimal floating-point is a lot more suitable for financial computing, while a binary floating-point is designed with scientific computing in mind. We want to do both! Given that a decimal floating-point provides a higher accuracy upon rendering, the .Net decimal requires a 96-bit significand only, which easily fits into the NaN space of our guadruple-precision floating-point UUID, using the following bit pattern:

+111 1111, 1111 1111; ssss ssss, ssss ssss
ssss ssss, ssss ssss; 1111 0001, ssss ssss
100e eeee, ssss ssss; ssss ssss, ssss ssss
ssss ssss, ssss ssss; ssss ssss, ssss ssss

The .Net decimal uses an ultra-short 5-bit exponent, which fits well after 0b100. This leaves 4 unused bits for minor versioning. We keep 0b0000 for infinities, namely, version-15.0, and 0b1111 for NaN, namely, version-15.15. Due to the significance of financial computing in general computing, we give version-15.1 to the .Net decimal. To set integers apart from real number approximations, binary or decimal, we give version-15.2 to big integers, similar to the bit pattern above:

+111 1111, 1111 1111; ssss ssss, ssss ssss
ssss ssss, ssss ssss; 1111 0010, ssss ssss
100s ssss, ssss ssss; ssss ssss, ssss ssss
ssss ssss, ssss ssss; ssss ssss, ssss ssss

How about version-15.3 to version-15.14, so many minor versions unused? Well, we can reserve them for future expansions, such as error codes, warning codes, activity codes, machine states, instruction codes, message codes, process ids, type ids... to name a few. The possibilities are many. We always reserve bits in bit pattern designs anyway. Here is a very good example.

The primitive data types look straightforward enough. We now want to cover composite objects. As we all know, there are two types of code in the universe, no more and no less. One is called address (one-to-one), and the other hash (one-to-many), meaning collisions allowed. Hashes come in many flavors: checksums (one-to-many), strong hashes (one-to-some) and secure hashes (one-to-few). Flavor is just a measure of security strength, nothing complicated by nature. On the contrary, the design of a secure hash algorithm is difficult and challenging, although beyond the scope of our tip-&-trick. If we have an address id, its UUID will look like this, whose internal strucure to be discussed soon:

???? ????, ???? ????; ???? ????, ???? ????
???? ????, ???? ????; 1111 ????, ???? ????
1010 ????, ???? ????; ???? ????, ???? ????
???? ????, ???? ????; ???? ????, ???? ????

And if we end up with a hash id, its UUID will look like this, where each h is a hash bit:

hhhh hhhh, hhhh hhhh; hhhh hhhh, hhhh hhhh
hhhh hhhh, hhhh hhhh; 1111 hhhh, hhhh hhhh
1011 hhhh, hhhh hhhh; hhhh hhhh, hhhh hhhh
hhhh hhhh, hhhh hhhh; hhhh hhhh, hhhh hhhh

Why do we need a hash id? Well, an address performs a lookup, while a hash executes a reverse. Say you have a string or a file, but you don't know its address. What can you do? With a hash-based reverse lookup mechanism, you can find a cloud object's address by its content. To make sure that we're talking about the same hash, we need to pick the best hash algorithm for our version-15 hash. A well-known speed test site comes to mind (MD5, SHA-1, SHA-256 and SHA-512 speed performance). Given that these algorithms roughly encode at the same speed on 64-bit machines, we naturally opt for the most secure hash, which is SHA-512, truncated to fit a UUID. It's well worth the time to compute more carefully. Imagine, you can narrow down to ten instead of a thousand cloud objects sharing the same hash by spending only twice amount of time encoding yours. What a huge gain! When the size of a hash table is the entire cloud, you want a very secure hash.

Version-1 UUID is perfect for cloud objects. Since a cloud object is open by nature, we don't really have a privacy issue to deal with. Even if privacy indeed became a concern in specific confidential scenarios, proxy MAC addresses could easily be employed to conceal originating network cards. Unfortunately, version-1 UUID isn't a friendly primary key. We need a better design in our version-15 UUID, one that starts with time to facilitate clustered indexing. Accuracy matters. We don't want to mandate a higher-than-subsecond accuracy for all cloud devices, for that's just unrealistic. A span of 20,000 years consists of 20,000 years times 365.24 days times 24 hours times 60 minutes times 60 seconds, a product of 631,134,720,000 seconds, 0x92_f28f_9000 in hex and 5 bytes to store. A MAC address, on the other hand, demands 6 bytes in storage. So, it means that we have 4 bytes left for the counter, entailing that we cannot produce more than 4 billion cloud objects per second on a single network card, totally reasonable. The bit pattern below specifies its format, with t to represent a time bit, m a MAC bit and c a counter bit:

tttt tttt, tttt tttt; tttt tttt, tttt tttt
tttt tttt, mmmm mmmm; 1111 mmmm, mmmm mmmm
1010 mmmm, mmmm mmmm; mmmm mmmm, mmmm mmmm
cccc cccc, cccc cccc; cccc cccc, cccc cccc

The DateTime value type in .Net happens to cover 10,000 years in range. If we divide our 20,000 years by two, half BC and half AD, then our AD part works seamlessly with .Net. To ensure clustered indexing, we will start our zero at 0x7f_ffff_ffff so all our positive integers with will begin with 0b1. This design resembles an IEEE floating-point exponent. Does it ring the bell? Lastly, there is that dreaded nil UUID. Though it can't throw a NullReferenceException like a reference type or a DivideByZeroException like a numeric type, it still can come up with nasty surprises, which you must watch out for. Like all value types, the nil UUID looks like this:

0000 0000, 0000 0000; 0000 0000, 0000 0000
0000 0000, 0000 0000; 0000 0000, 0000 0000
0000 0000, 0000 0000; 0000 0000, 0000 0000
0000 0000, 0000 0000; 0000 0000, 0000 0000

Design is easy. How about its implementation? To pave our way forward, let's start with a series of C# type-safe "macros". Firstly, all bit operations must be signed to comply with CLS. Secondly, C# bit operators have a horrible and unnatural precedence setup, which we may simply forgo by wrapping them in extension methods so as to embrace member access autocomplete from today on. I don't know what you think, but I frankly can't code without autocomplete. Now, we want to ensure that all integral types cast to int or long by default, with those two final targets casting to each other. Note well that these arn't your regular integral type conversions, because sign bits don't move. For this good reason, we call them the At method group, instead of As.

C#
namespace Pyramid.Kernel.Up
{
 partial class It
 {
  /// <summary>At an <see cref="int"/> to <see langword="return"/>.</summary>
  /// <param name="it">A <see cref="byte"/>.</param>
  public static int At(this byte it) => it;
  /// <summary>At the <paramref name="alias"/> to <see langword="return"/>.</summary>
  /// <param name="it">A <see cref="byte"/></param>
  /// <param name="alias">An alias <see cref="int"/>.</param>
  public static int At(this byte it, out int alias) => alias = it;
  /// <summary>At the <paramref name="alias"/> to <see langword="return"/>.</summary>
  /// <param name="it">A <see cref="byte"/></param>
  /// <param name="alias">An alias <see cref="long"/>.</param>
  public static long At(this byte it, out long alias) => alias = it;
  /// <summary>At the <paramref name="alias"/> to <see langword="return"/>.</summary>
  /// <param name="it">A <see cref="byte"/></param>
  /// <param name="alias">An alias <see cref="short"/>.</param>
  public static short At(this byte it, out short alias) => alias = it;
  /// <summary>At an <see cref="int"/> to <see langword="return"/>.</summary>
  /// <param name="it">A <see cref="char"/>.</param>
  public static int At(this char it) => it;
  /// <summary>At a <see cref="long"/> to <see langword="return"/>.</summary>
  /// <param name="it">An <see cref="int"/>.</param>
  public static long At(this int it) => unchecked((uint)it);
  /// <summary>At the <paramref name="alias"/> to <see langword="return"/>.</summary>
  /// <param name="it">An <see cref="int"/></param>
  /// <param name="alias">An alias <see cref="byte"/>.</param>
  public static byte At(this int it, out byte alias) => unchecked(alias = (byte)(uint)it);
  /// <summary>At the <paramref name="alias"/> to <see langword="return"/>.</summary>
  /// <param name="it">An <see cref="int"/></param>
  /// <param name="alias">An alias <see cref="long"/>.</param>
  public static long At(this int it, out long alias) => alias = it.At();
  /// <summary>At the <paramref name="alias"/> to <see langword="return"/>.</summary>
  /// <param name="it">An <see cref="int"/></param>
  /// <param name="alias">An alias <see cref="short"/>.</param>
  public static short At(this int it, out short alias) =>
   unchecked(alias = (short)(ushort)(uint)it);
  /// <summary>At an <see cref="int"/> to <see langword="return"/>.</summary>
  /// <param name="it">A <see cref="long"/>.</param>
  public static int At(this long it) => unchecked((int)(uint)(ulong)it);
  /// <summary>At the <paramref name="alias"/> to <see langword="return"/>.</summary>
  /// <param name="it">A <see cref="long"/></param>
  /// <param name="alias">An alias <see cref="byte"/>.</param>
  public static byte At(this long it, out byte alias) => unchecked(alias = (byte)(ulong)it);
  /// <summary>At the <paramref name="alias"/> to <see langword="return"/>.</summary>
  /// <param name="it">A <see cref="long"/></param>
  /// <param name="alias">An alias <see cref="int"/>.</param>
  public static int At(this long it, out int alias) => alias = it.At();
  /// <summary>At the <paramref name="alias"/> to <see langword="return"/>.</summary>
  /// <param name="it">A <see cref="long"/></param>
  /// <param name="alias">An alias <see cref="short"/>.</param>
  public static short At(this long it, out short alias) =>
   unchecked(alias = (short)(ushort)(ulong)it);
  internal static int _At(this sbyte it) => unchecked((byte)it);
  /// <summary>At an <see cref="int"/> to <see langword="return"/>.</summary>
  /// <param name="it">A <see cref="short"/>.</param>
  public static int At(this short it) => unchecked((ushort)it);
  /// <summary>At the <paramref name="alias"/> to <see langword="return"/>.</summary>
  /// <param name="it">A <see cref="short"/></param>
  /// <param name="alias">An alias <see cref="byte"/>.</param>
  public static byte At(this short it, out byte alias) => unchecked(alias = (byte)(ushort)it);
  /// <summary>At the <paramref name="alias"/> to <see langword="return"/>.</summary>
  /// <param name="it">A <see cref="short"/></param>
  /// <param name="alias">An alias <see cref="int"/>.</param>
  public static int At(this short it, out int alias) => alias = it.At();
  /// <summary>At the <paramref name="alias"/> to <see langword="return"/>.</summary>
  /// <param name="it">A <see cref="short"/></param>
  /// <param name="alias">An alias <see cref="long"/>.</param>
  public static long At(this short it, out long alias) => unchecked(alias = (ushort)it);
  internal static int _At(this uint it) => unchecked((int)it);
  internal static long _At(this ulong it) => unchecked((long)it);
  internal static int _At(this ushort it) => it;
 }
}

Fantastic! Let's start with equality and inequality tests, arguably the fastest bit operations available. Given that C# supports bit operators for int, uint, long and ulong only, the very signed integers available are thus int and long, end of story:

C#
namespace Pyramid.Kernel.Up
{
 partial class It
 {
  /// <summary>Is <paramref name="that"/>?</summary>
  /// <param name="it">An <see cref="int"/>.</param>
  /// <param name="that">Another <see cref="int"/>.</param>
  public static bool Is(this int it, int that) => it == that;
  /// <summary>Is <paramref name="that"/>?</summary>
  /// <param name="it">A <see cref="long"/>.</param>
  /// <param name="that">Another <see cref="long"/>.</param>
  public static bool Is(this long it, long that) => it == that;
  /// <summary>Isn't <paramref name="that"/>?</summary>
  /// <param name="it">An <see cref="int"/>.</param>
  /// <param name="that">Another <see cref="int"/>.</param>
  public static bool Isnt(this int it, int that) => it != that;
  /// <summary>Isn't <paramref name="that"/>?</summary>
  /// <param name="it">A <see cref="long"/>.</param>
  /// <param name="that">Another <see cref="long"/>.</param>
  public static bool Isnt(this long it, long that) => it != that;
 }
}

By supporting signed integers exclusively, we make sure that if we're using unsigned integers by mistake, autocomplete won't show our extension methods. Also, we've chosen to favor the 32-bit int whenever possible, because it ends up with shorter machine code, hence both CPU-friendly by promoting parallel execution and cache-friendly by squeezing more instructions into CPU pipelines. Let's add some bitwise logical operations, second in speed to equality and inequality tests:

C#
namespace Pyramid.Kernel.Up
{
 partial class It
 {
  /// <summary>And <paramref name="that"/> bitwise to <see langword="return"/>.</summary>
  /// <param name="it">An <see cref="int"/>.</param>
  /// <param name="that">Another <see cref="int"/>.</param>
  public static int And(this int it, int that) => it & that;
  /// <summary>And <paramref name="that"/> bitwise to <see langword="return"/>.</summary>
  /// <param name="it">A <see cref="long"/>.</param>
  /// <param name="that">Another <see cref="long"/>.</param>
  public static long And(this long it, long that) => it & that;
  /// <summary>Not bitwise to <see langword="return"/>.</summary>
  /// <param name="it">An <see cref="int"/>.</param>
  public static int Not(this int it) => ~it;
  /// <summary>Not bitwise to <see langword="return"/>.</summary>
  /// <param name="it">A <see cref="long"/>.</param>
  public static long Not(this long it) => ~it;
  /// <summary>Or <paramref name="that"/> bitwise to <see langword="return"/>.</summary>
  /// <param name="it">An <see cref="int"/>.</param>
  /// <param name="that">Another <see cref="int"/>.</param>
  public static int Or(this int it, int that) => it | that;
  /// <summary>Or <paramref name="that"/> bitwise to <see langword="return"/>.</summary>
  /// <param name="it">A <see cref="long"/>.</param>
  /// <param name="that">Another <see cref="long"/>.</param>
  public static long Or(this long it, long that) => it | that;
  /// <summary>Xor <paramref name="that"/> bitwise to <see langword="return"/>.</summary>
  /// <param name="it">An <see cref="int"/>.</param>
  /// <param name="that">Another <see cref="int"/>.</param>
  public static int Xor(this int it, int that) => it ^ that;
  /// <summary>Xor <paramref name="that"/> bitwise to <see langword="return"/>.</summary>
  /// <param name="it">A <see cref="long"/>.</param>
  /// <param name="that">Another <see cref="long"/>.</param>
  public static long Xor(this long it, long that) => it ^ that;
 }
}

The third in speed are shift operations, which prove very useful in almost every bitwise scenario:

C#
namespace Pyramid.Kernel.Up
{
 partial class It
 {
  /// <summary>Down logically by <paramref name="l"/> to <see langword="return"/>.</summary>
  /// <param name="it">An <see cref="int"/>.</param>
  /// <param name="l">A length <see cref="int"/>.</param>
  public static int Down(this int it, int l) => unchecked((int)((uint)it >> l));
  /// <summary>Down logically by <paramref name="l"/> to <see langword="return"/>.</summary>
  /// <param name="it">A <see cref="long"/>.</param>
  /// <param name="l">A length <see cref="int"/>.</param>
  public static long Down(this long it, int l) => unchecked((long)((ulong)it >> l));
  /// <summary>Down logically by <paramref name="l"/> at an <see cref="int"/> to
  /// <see langword="return"/>.</summary>
  /// <param name="it">A <see cref="long"/>.</param>
  /// <param name="l">A length <see cref="int"/>.</param>
  public static int DownAt(this long it, int l) => it.Down(l).At();
  /// <summary>Left arithmetically by <paramref name="l"/> to <see langword="return"/>.
  /// </summary>
  /// <param name="it">An <see cref="int"/>.</param>
  /// <param name="l">A length <see cref="int"/>.</param>
  public static int Left(this int it, int l) => it << l;
  /// <summary>Left arithmetically by <paramref name="l"/> to <see langword="return"/>.
  /// </summary>
  /// <param name="it">A <see cref="long"/>.</param>
  /// <param name="l">A length <see cref="int"/>.</param>
  public static long Left(this long it, int l) => it << l;
  /// <summary>Left arithmetically by <paramref name="l"/> at a <see cref="long"/> to
  /// <see langword="return"/>.</summary>
  /// <param name="it">An <see cref="int"/>.</param>
  /// <param name="l">A length <see cref="int"/>.</param>
  public static long LeftAt(this int it, int l) => it.At().Left(l);
  /// <summary>Right arithmetically by <paramref name="l"/> to <see langword="return"/>.
  /// </summary>
  /// <param name="it">An <see cref="int"/>.</param>
  /// <param name="l">A length <see cref="int"/>.</param>
  public static int Right(this int it, int l) => it >> l;
  /// <summary>Right arithmetically by <paramref name="l"/> to <see langword="return"/>.
  /// </summary>
  /// <param name="it">A <see cref="long"/>.</param>
  /// <param name="l">A length <see cref="int"/>.</param>
  public static long Right(this long it, int l) => it >> l;
  /// <summary>Right arithmetically by <paramref name="l"/> at an <see cref="int"/> to
  /// <see langword="return"/>.</summary>
  /// <param name="it">A <see cref="long"/>.</param>
  /// <param name="l">A length <see cref="int"/>.</param>
  public static int RightAt(this long it, int l) => it.Right(l).At();
  /// <summary>Up logically by <paramref name="l"/> to <see langword="return"/>.</summary>
  /// <param name="it">An <see cref="int"/>.</param>
  /// <param name="l">A length <see cref="int"/>.</param>
  public static int Up(this int it, int l) => unchecked((int)((uint)it << l));
  /// <summary>Up logically by <paramref name="l"/> to <see langword="return"/>.</summary>
  /// <param name="it">A <see cref="long"/>.</param>
  /// <param name="l">A length <see cref="int"/>.</param>
  public static long Up(this long it, int l) => unchecked((long)((ulong)it << l));
  /// <summary>Up logically by <paramref name="l"/> at a <see cref="long"/> to
  /// <see langword="return"/>.</summary>
  /// <param name="it">An <see cref="int"/>.</param>
  /// <param name="l">A length <see cref="int"/>.</param>
  public static long UpAt(this int it, int l) => it.At().Up(l);
 }
}

Why favor going up and down rather than left and right with logical shifts? Well, we try not to suggest endianness, since it's way too platform-dependent. Besides, we prefer shorter names. In the case of arithmetic shifts, we already have the planetary convention to write from left to right, thus keeping their original names. Lastly, we have the slowest "very high-level bit operations" unchecked for the best possible performance, when we know exactly what we're doing:

C#
namespace Pyramid.Kernel.Up
{
 partial class It
 {
  /// <summary>Minus inversely by a 0 <see cref="int"/> to <see langword="return"/>.</summary>
  /// <param name="it">An <see cref="int"/>.</param>
  public static int Minus(this int it) => unchecked(-it);
  /// <summary>Minus <paramref name="that"/> to <see langword="return"/>.</summary>
  /// <param name="it">An <see cref="int"/>.</param>
  /// <param name="that">Another <see cref="int"/>.</param>
  public static int Minus(this int it, int that) => unchecked(it - that);
  /// <summary>Minus inversely by a 0 <see cref="long"/> to <see langword="return"/>.</summary>
  /// <param name="it">A <see cref="long"/>.</param>
  public static long Minus(this long it) => unchecked(-it);
  /// <summary>Minus <paramref name="that"/> to <see langword="return"/>.</summary>
  /// <param name="it">A <see cref="long"/>.</param>
  /// <param name="that">Another <see cref="long"/>.</param>
  public static long Minus(this long it, long that) => unchecked(it - that);
  /// <summary>Modulo <paramref name="that"/> to <see langword="return"/>.</summary>
  /// <param name="it">An <see cref="int"/>.</param>
  /// <param name="that">Another <see cref="int"/>.</param>
  public static int Modulo(this int it, int that) => unchecked(it % that);
  /// <summary>Modulo <paramref name="that"/> to <see langword="return"/>.</summary>
  /// <param name="it">A <see cref="long"/>.</param>
  /// <param name="that">Another <see cref="long"/>.</param>
  public static long Modulo(this long it, long that) => unchecked(it % that);
  /// <summary>Over <paramref name="that"/> to <see langword="return"/>.</summary>
  /// <param name="it">An <see cref="int"/>.</param>
  /// <param name="that">Another <see cref="int"/>.</param>
  public static int Over(this int it, int that) => unchecked(it / that);
  /// <summary>Over <paramref name="that"/> to <see langword="return"/>.</summary>
  /// <param name="it">A <see cref="long"/>.</param>
  /// <param name="that">Another <see cref="long"/>.</param>
  public static long Over(this long it, long that) => unchecked(it / that);
  /// <summary>Plus <paramref name="that"/> to <see langword="return"/>.</summary>
  /// <param name="it">An <see cref="int"/>.</param>
  /// <param name="that">Another <see cref="int"/>.</param>
  public static int Plus(this int it, int that) => unchecked(it + that);
  /// <summary>Plus <paramref name="that"/> to <see langword="return"/>.</summary>
  /// <param name="it">A <see cref="long"/>.</param>
  /// <param name="that">Another <see cref="long"/>.</param>
  public static long Plus(this long it, long that) => unchecked(it + that);
  /// <summary>Times <paramref name="that"/> to <see langword="return"/>.</summary>
  /// <param name="it">An <see cref="int"/>.</param>
  /// <param name="that">Another <see cref="int"/>.</param>
  public static int Times(this int it, int that) => unchecked(it * that);
  /// <summary>Times <paramref name="that"/> to <see langword="return"/>.</summary>
  /// <param name="it">A <see cref="long"/>.</param>
  /// <param name="that">Another <see cref="long"/>.</param>
  public static long Times(this long it, long that) => unchecked(it * that);
 }
}

Now, we are ready to extract bits out of signed integers:

C#
namespace Pyramid.Kernel.Up
{
 partial class It
 {
  /// <summary>Has <paramref name="that"/>?</summary>
  /// <param name="it">An <see cref="int"/>.</param>
  /// <param name="that">Another <see cref="int"/>.</param>
  public static bool Has(this int it, int that) => it.And(that).Is(that);
  /// <summary>Has <paramref name="that"/> by <paramref name="l"/>?</summary>
  /// <param name="it">An <see cref="int"/>.</param>
  /// <param name="that">Another <see cref="int"/>.</param>
  /// <param name="l">A length <see cref="int"/>.</param>
  public static bool Has(this int it, int that, int l) => it.Have(that, l).Is(that);
  /// <summary>Has <paramref name="that"/>?</summary>
  /// <param name="it">A <see cref="long"/>.</param>
  /// <param name="that">Another <see cref="long"/>.</param>
  public static bool Has(this long it, long that) => it.And(that).Is(that);
  /// <summary>Has <paramref name="that"/> by <paramref name="l"/>?</summary>
  /// <param name="it">A <see cref="long"/>.</param>
  /// <param name="that">Another <see cref="long"/>.</param>
  /// <param name="l">A length <see cref="int"/>.</param>
  public static bool Has(this long it, long that, int l) => it.Have(that, l).Is(that);
  /// <summary>Have the <paramref name="alias"/> by <paramref name="l"/> to
  /// <see langword="return"/>.</summary>
  /// <param name="it">An <see cref="int"/></param>
  /// <param name="alias">An alias <see cref="byte"/>.</param>
  /// <param name="l">A length <see cref="int"/>.</param>
  public static byte Have(this int it, out byte alias, int l) => it.Down(l).At(out alias);
  /// <summary>Have in <paramref name="mask"/> by <paramref name="l"/> to
  /// <see langword="return"/>.</summary>
  /// <param name="it">An <see cref="int"/>.</param>
  /// <param name="mask">A mask <see cref="int"/>.</param>
  /// <param name="l">A length <see cref="int"/>.</param>
  public static int Have(this int it, int mask, int l) => it.Down(l).And(mask);
  /// <summary>Have the <paramref name="alias"/> by <paramref name="l"/> to
  /// <see langword="return"/>.</summary>
  /// <param name="it">An <see cref="int"/></param>
  /// <param name="alias">An alias <see cref="short"/>.</param>
  /// <param name="l">A length <see cref="int"/>.</param>
  public static short Have(this int it, out short alias, int l) => it.Down(l).At(out alias);
  /// <summary>Have the <paramref name="alias"/> by <paramref name="l"/> to
  /// <see langword="return"/>.</summary>
  /// <param name="it">A <see cref="long"/></param>
  /// <param name="alias">An alias <see cref="byte"/>.</param>
  /// <param name="l">A length <see cref="int"/>.</param>
  public static byte Have(this long it, out byte alias, int l) => it.Down(l).At(out alias);
  /// <summary>Have the <paramref name="alias"/> by <paramref name="l"/> to
  /// <see langword="return"/>.</summary>
  /// <param name="it">A <see cref="long"/></param>
  /// <param name="alias">An alias <see cref="int"/>.</param>
  /// <param name="l">A length <see cref="int"/>.</param>
  public static int Have(this long it, out int alias, int l) => it.Down(l).At(out alias);
  /// <summary>Have in <paramref name="mask"/> by <paramref name="l"/> to
  /// <see langword="return"/>.</summary>
  /// <param name="it">A <see cref="long"/>.</param>
  /// <param name="mask">A mask <see cref="long"/>.</param>
  /// <param name="l">A length <see cref="int"/>.</param>
  public static long Have(this long it, long mask, int l) => it.Down(l).And(mask);
  /// <summary>Have the <paramref name="alias"/> by <paramref name="l"/> to
  /// <see langword="return"/>.</summary>
  /// <param name="it">A <see cref="long"/></param>
  /// <param name="alias">An alias <see cref="short"/>.</param>
  /// <param name="l">A length <see cref="int"/>.</param>
  public static short Have(this long it, out short alias, int l) => it.Down(l).At(out alias);
  /// <summary>Have the <paramref name="alias"/> by <paramref name="l"/> to
  /// <see langword="return"/>.</summary>
  /// <param name="it">A <see cref="short"/></param>
  /// <param name="alias">An alias <see cref="byte"/>.</param>
  /// <param name="l">A length <see cref="int"/>.</param>
  public static byte Have(this short it, out byte alias, int l) =>
   it.At().Down(l).At(out alias);
  /// <summary>Have in <paramref name="mask"/> by <paramref name="l"/> at an <see cref="int"/>
  /// to <see langword="return"/>.</summary>
  /// <param name="it">A <see cref="long"/>.</param>
  /// <param name="mask">A mask <see cref="int"/>.</param>
  /// <param name="l">A length <see cref="int"/>.</param>
  public static int HaveAt(this long it, int mask, int l) => it.DownAt(l).And(mask);
 }
}

After the extraction of bits, we frequently shift them up before reorganizing them into new signed integers:

C#
namespace Pyramid.Kernel.Up
{
 partial class It
 {
  /// <summary>Put in <paramref name="mask"/> by <paramref name="l"/> to
  /// <see langword="return"/>.</summary>
  /// <param name="it">An <see cref="int"/>.</param>
  /// <param name="mask">A mask <see cref="int"/>.</param>
  /// <param name="l">A length <see cref="int"/>.</param>
  public static int Put(this int it, int mask, int l) => it.And(mask).Up(l);
  /// <summary>Put in <paramref name="mask"/> by <paramref name="l"/> to
  /// <see langword="return"/>.</summary>
  /// <param name="it">A <see cref="long"/>.</param>
  /// <param name="mask">A mask <see cref="long"/>.</param>
  /// <param name="l">A length <see cref="int"/>.</param>
  public static long Put(this long it, long mask, int l) => it.And(mask).Up(l);
  /// <summary>Put in <paramref name="mask"/> by <paramref name="l"/> at a <see cref="long"/>
  /// to <see langword="return"/>.</summary>
  /// <param name="it">A <see cref="int"/>.</param>
  /// <param name="mask">A mask <see cref="int"/>.</param>
  /// <param name="l">A length <see cref="int"/>.</param>
  public static long PutAt(this int it, int mask, int l) => it.And(mask).UpAt(l);
 }
}

Things get a little more exciting, because we want to read bits from a Guid's Span<byte> or a decimal's Span<int>. Since Intel and AMD processors are little-endian in contrast to everything in the cloud being big-endian, you can't simply call BitConverter to read a Span<byte> for you. If you do, you'll have to call Array.Reverse or IPAddress.HostToNetworkOrder on everything you read. It is, of course, OK to do so, but they are just too slow. Plus, what if you forget to change endianness sometimes? We can easily beat their speed with our extension methods below, each within a single line:

C#
using System;
using System.Runtime.CompilerServices;
namespace Pyramid.Kernel.Up
{
 partial class It
 {
  /// <summary>Get the <paramref name="alias"/> at <paramref name="i"/> to
  /// <see langword="return"/>.</summary>
  /// <param name="it">A <see cref="byte"/> <see cref="Span{T}"/>.</param>
  /// <param name="alias">An alias <see cref="byte"/>.</param>
  /// <param name="i">An index <see cref="int"/>.</param>
  public static int Get(this Span<byte> it, out byte alias, int i) => (alias = it[i]).At();
  /// <summary>Get the <paramref name="alias"/> at <paramref name="i"/> to
  /// <see langword="return"/>.</summary>
  /// <param name="it">A <see cref="byte"/> <see cref="Span{T}"/>.</param>
  /// <param name="alias">An alias <see cref="int"/>.</param>
  /// <param name="i">An index <see cref="int"/>.</param>
  [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
  public static int Get(this Span<byte> it, out int alias, int i) =>
   alias = it.Get(out short _, i).Up(16).Or(it.Get(out short _, i + 2));
  /// <summary>Get the <paramref name="alias"/> at <paramref name="i"/> to
  /// <see langword="return"/>.</summary>
  /// <param name="it">A <see cref="byte"/> <see cref="Span{T}"/>.</param>
  /// <param name="alias">An alias <see cref="long"/>.</param>
  /// <param name="i">An index <see cref="int"/>.</param>
  [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
  public static long Get(this Span<byte> it, out long alias, int i) =>
   alias = it.Get(out int _, i).UpAt(32).Or(it.Get(out int _, i + 4).At());
  /// <summary>Get the <paramref name="alias"/> at <paramref name="i"/> to
  /// <see langword="return"/>.</summary>
  /// <param name="it">A <see cref="byte"/> <see cref="Span{T}"/>.</param>
  /// <param name="alias">An alias <see cref="short"/>.</param>
  /// <param name="i">An index <see cref="int"/>.</param>
  [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
  public static int Get(this Span<byte> it, out short alias, int i) =>
   it.Get(out byte _, i).Up(8).Or(it.Get(out byte _, i + 1)).At(out alias).At();
  /// <summary>Get the <paramref name="alias"/> at <paramref name="i"/> to
  /// <see langword="return"/>.</summary>
  /// <param name="it">An <see cref="int"/> <see cref="Span{T}"/>.</param>
  /// <param name="alias">An alias <see cref="int"/>.</param>
  /// <param name="i">An index <see cref="int"/>.</param>
  public static int Get(this Span<int> it, out int alias, int i) => alias = it[i];
  /// <summary>Get the <paramref name="alias"/> at <paramref name="i"/> to
  /// <see langword="return"/>.</summary>
  /// <param name="it">An <see cref="int"/> <see cref="Span{T}"/>.</param>
  /// <param name="alias">An alias <see cref="long"/>.</param>
  /// <param name="i">An index <see cref="int"/>.</param>
  [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
  public static long Get(this Span<int> it, out long alias, int i) =>
   alias = it.Get(out int _, i).UpAt(32).Or(it.Get(out int _, i + 1).At());
 }
}

The trick is to convert as we read, reading everything once only, whose call structure is then hard-wired into a binary tree for optimal performance. Notice that we tag them with MethodImplOptions.AggressiveInlining and MethodImplOptions.AggressiveOptimization, just to make sure that these methods get inlined and reoptimized at a higher scope, recursively all the way up to the root of the tree. After getting bits from Span<byte> and Span<int>, we want to transform them and write them back. To do so, we have the following Set group, similar to their Get counterparts:

C#
namespace Pyramid.Kernel.Up
{
 partial class It
 {
  /// <summary>Set the <paramref name="value"/> at <paramref name="i"/> to
  /// <see langword="return"/>.</summary>
  /// <param name="it">A <see cref="byte"/> <see cref="Span{T}"/>.</param>
  /// <param name="value">A value <see cref="byte"/>.</param>
  /// <param name="i">An index <see cref="int"/>.</param>
  public static byte Set(this Span<byte> it, byte value, int i) => it[i] = value;
  /// <summary>Set the <paramref name="value"/> at <paramref name="i"/> to
  /// <see langword="return"/>.</summary>
  /// <param name="it">A <see cref="byte"/> <see cref="Span{T}"/>.</param>
  /// <param name="value">A value <see cref="int"/>.</param>
  /// <param name="i">An index <see cref="int"/>.</param>
  [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
  public static int Set(this Span<byte> it, int value, int i) =>
   it.Set(value.Have(out short _, 16), i).So(it.Set(value.At(out short _), i + 2)).So(value);
  /// <summary>Set the <paramref name="value"/> at <paramref name="i"/> to
  /// <see langword="return"/>.</summary>
  /// <param name="it">A <see cref="byte"/> <see cref="Span{T}"/>.</param>
  /// <param name="value">A value <see cref="long"/>.</param>
  /// <param name="i">An index <see cref="int"/>.</param>
  [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
  public static long Set(this Span<byte> it, long value, int i) =>
   it.Set(value.Have(out int _, 32), i).So(it.Set(value.At(out int _), i + 4)).So(value);
  /// <summary>Set the <paramref name="value"/> at <paramref name="i"/> to
  /// <see langword="return"/>.</summary>
  /// <param name="it">A <see cref="byte"/> <see cref="Span{T}"/>.</param>
  /// <param name="value">A value <see cref="short"/>.</param>
  /// <param name="i">An index <see cref="int"/>.</param>
  [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
  public static short Set(this Span<byte> it, short value, int i) =>
   it.Set(value.Have(out byte _, 8), i).So(it.Set(value.At(out byte _), i + 1)).So(value);
  /// <summary>Set the <paramref name="value"/> at <paramref name="i"/> to
  /// <see langword="return"/>.</summary>
  /// <param name="it">An <see cref="int"/> <see cref="Span{T}"/>.</param>
  /// <param name="value">A value <see cref="int"/>.</param>
  /// <param name="i">An index <see cref="int"/>.</param>
  public static int Set(this Span<int> it, int value, int i) => it[i] = value;
  /// <summary>Set the <paramref name="value"/> at <paramref name="i"/> to
  /// <see langword="return"/>.</summary>
  /// <param name="it">An <see cref="int"/> <see cref="Span{T}"/>.</param>
  /// <param name="value">A value <see cref="long"/>.</param>
  /// <param name="i">An index <see cref="int"/>.</param>
  [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
  public static long Set(this Span<int> it, long value, int i) =>
   it.Set(value.Have(out int _, 32), i).So(it.Set(value.At(out int _), i + 1)).So(value);
 }
}

To complete our story, we must include checks against a nil Guid, a zero numeric type or anything possibly empty, even for arrays, strings, collections, lists, sets, maps and whatever. For the sake of our exercise, we handle IEquatable<T> value types only.

C#
namespace Pyramid.Kernel.Up
{
 partial class It
 {
  /// <summary>Has (non-empty)?</summary>
  /// <typeparam name="J">Its self-referencing <see cref="IEquatable{T}"/>.</typeparam>
  /// <param name="it">A <typeparamref name="J"/>.</param>
  public static bool Has<J>(this J it) where J : struct, IEquatable<J> => !it.Hasnt();
  /// <summary>Hasn't (empty)?</summary>
  /// <typeparam name="J">Its self-referencing <see cref="IEquatable{T}"/>.</typeparam>
  /// <param name="it">A <typeparamref name="J"/>.</param>
  public static bool Hasnt<J>(this J it) where J : struct, IEquatable<J> => it.Equals(default);
 }
}

Bit operations are really set and sequence operations, which can be further generalized and generified. To cover them in depth, we want to introduce method body objects. However, I feel it's important to beat the iron while it's hot. Let's go over some cloud id conversions next before anything else. After all, I can't leave your curiosity hungry following such an appetizer, can I?

Points of Interest

  1. UUID is an Internet standard used in data replication services to guarantee high data availability, performance and scalability, which can be leveraged to address any cloud object, including primitive data values, system states, error codes, type ids or just about anything you can imagine.
  2. UUID, like any other id, comes in two categories, addresses and hashes, with addresses to uniquely identify any cloud object and hashes to handle reverse lookups.
  3. Version-1 UUID is suitable for primary keys to support clustered indexing, if its bit order is reorganized, which is perfect as an address for any cloud object, open by nature without privacy concerns. Even when confidentiality becomes a requirement, proxy MAC addresses can easily be employed to conceal originating network cards.

History

  • 28th August, 2021: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Code Fan
Software Developer
Canada Canada
Montreal is the second largest French city in the world, next to Paris. I like the fact that real estate is dirt cheap here, so cheap that software development alone enables a financial capacity to afford a nearly 2,000-square-foot luxurious condo right in the middle of Downtown Montreal, a 5-minute walk from my office, beside the largest and oldest art museum in Canada with visitors and tourists from all over the planet, including Hollywood stars. I've chosen C# as my first language at Code Project, because it is the only garbage-collected language and platform meeting the performance requirements for real-time game programming, proven by Unity. Code must be perfect, providing safety, security, performance, scalability, availability, reliability, maintainability, extensibility, portability, compatibility, interoperability, readability, productivity, just to name a few. C# is the only language that comes close, with Rust second to it. That being said, even C# is far from being perfect. I dream my own programming language, while on my journey to it. We will see how it goes!

Comments and Discussions

 
-- There are no messages in this forum --