There is also concepts of systems with unbalanced ternary that use 0, 1 and 2. This is also a fair choice, but for this project we will use balanced ternary, as it has benefits in natural system for negative numbers, and clean arithmetics for a lot of things. In the future some implementations might be made for an unsigned word, but for now, it will stay with using balanced ternary and signed words.
A lot of other terms are also worth defining for this project. I will try and compare them with binary logic, to make it more generally approachable.
Kleenes are ternaries answer to booleans. A boolean, or bool for short, has two states, False (0) and True (1). This is what we’re used to with logic, an if statement for example will say “if x == y”, which can be understand it two ways. Either x and y are equal, returning True. Or they’re different, and it returns False (0).
But what about other cases? What if the types don’t match? What if the logic isn’t directly binary? One example of this is a state like “is x true?”, it seems simple, but what if x is undefined, or only going to be defined in the future? Natural instinct is to return False, as it can’t be True, but actually, it can’t be False either before it’s defined.
In this case, a Kleene would fit well. It describes three states:
False (0)
True (1)
Unknown (-1)
In the specific case of x being undefined, or it’s definition / implementation is in the future, you’d likely want to throw an exception or error in proper binary programming. But that shouldn’t detract from the cases where Kleenes still can offer a benefit.
And their benefits are well known for a lot of user input related fields. Many languages have a notion of Undefined, as an extra state added to Booleans. Example include SQL, JS, and certain forms of config files. But even within logic, there are cases where something is neither True, nor False. Generally any form of answer check is quite fitting for Kleenes, “did this test pass or fail?”, it hasn’t run yet, so we can mark it’s state as Unknown (-1).
Kleenes also have interesting truth tables for logic gates, I could generally recommend reading both the Wikipedia article for Three-valued logic and An Introduction to Many-Valued and Fuzzy Logic. With the unknown state especially unlocking many patterns unseen in binary logic.
Log3n ternary storage efficiency. Ternary holding more values for a similar amount of data points, Word27 having between -3,812,798,742,493 and 3,812,798,742,493 values. While 32 bits only has between -2,147,483,647 and 2,147,483,647.
A Word refers to the collection of bits / trits in an array like manner. A Byte is a form of Word, specifically a binary Word of 8 bits. With this, an int8 can also be described as a binary Word8. However, when we use Word in this project, it refers to a ternary Word of trits that are devisable by 3. The main forms are:
Tribble (similar to a nibble, neither are really considered as direct types though)
Word9 (also called Tryte, similar to a Byte / i8)
Word27 (similar to i32)
Word54 (similar to i64)
Word108 (similar to i128)
Words will be used in a similar way as ints in this project, and Tryte specifically, will be the ternary answer to Bytes.
With this defined, we can define a system architecture like we’re used to with Binary. My computer for example has a 64-bit CPU and 16 gigabytes of RAM. A ternary computer in a similar range would have a 54-trit CPU and 18 gigatrytes of RAM. (note that I don’t know if I did the conversion that well, and that ternary memory would hold more storage due to having one more state for using trits over bits, and a tryte having one more trit, than bytes have bits)
Ternary Words also have a couple of interesting benefits, my favorite relating to it’s signed nature by default. How do you know whether a ternary Word is positive or negative? Look through it’s trits, the first non zero trit from left to right will correspond to it’s sign, and if it’s 0, then that perfectly fits with the 0 value of a trit as well. Want to flip the sign of a Word? Easy, just flip the sign of each trit in the word. Want the absolute value of the Word? Just flip all the negative trits. Also the fact that it’s max and min value are the same, just with one positive, the other negative is also deliciously simple.
If you find this type of manipulation of Words as interesting as I do, then I can strongly recommend playing with a balanced ternary calculator.
We’ve already seen that trits can be used to pack more data, we’ve seen it’s better for some logic, and even some of it’s arithmetic is really clean, but what does it matter if it can’t actually have an advantage in practice. Well actually, I do believe that it (could) have an advantage in practice, and that’s what I hope to prove with this project.
Right now we know of some parts that are more efficient, theoretically some code snippets could be optimized with using better branching features, letting you branch on more cases with fewer instructions than in binary, AI inference and machine learning optimizations in general (example, BitNet-b1.58). I would argue for certain math arithmetics as well, but in practice, it’s unlikely to be faster, and that’s due to the big reason ternary is going to have an uphill battle…
Binary logic already has a huge ecosystem. Practically all of our software is written for binary hardware, binary logic, binary I/O and way more. Until new systems are created and supported, ternary is unlikely to get a huge foothold. Binary will keep its advantage in I/O that interfaces with devices and definitions that are binary native, like UART, USB, displays and way more. Not to mention, binary processors and other hardware has been manufactured and perfected for decades. CMOS is a huge industry within logic gates alone, one that cannot be translated to ternary reasonable, and instead requires new technologies.
How long will it take to have a physical ternary CPU, that can even just somewhat rival what we have now with binary? likely, not in the next few years at least. A lot of research has been made in this field, but to get the processor node size down, making it competitive with binary, and getting software support are all huge undertakings. And it leads to a chicken and the egg question, what needs to come first? The hardware, or the software? I think it’s clear that the answer is both, and I’m better at software than hardware, so that’s where I put my effort.
I think ternary systems will have similar adoption struggles as RISC-V and ARM. By now ARM and AARCH is really well adopted. Devices running it are quite efficient, run well and often give a better experience for the user. For the longest time it was relegated to mobile devices, but since then, they’ve gathered a major foothold in the laptop and server markets as well. RISC-V is still in the early phases in adoption, it has some hardware boards, and software support is growing, but it’s still far from a practical day to day platform.
So what is my point with this? I think ternary will be a slow process to get into being a product people will want to use, BUT, I don’t think it’s impossible. My personal belief is that ternary will be like ARM, at first a bit of a niche with some proven benefits, but until it can stand toe to toe with it’s competitors like x86, it might as well just be a research technology. I think (and hope) that ternary will be proven and grow through smaller efforts, both in research, and specific cases like AI and efficiency. In my ideal world, it would take a while, but it would become more and more proven year by year, until before you know it, it actually has planted itself into certain workflows and positions solidly.
For a start, let’s define what a bit is (this will help build the foundation for trits). As most people will likely know, a bit is the smallest unit of data on a binary platform. A typical binary computer, will think in only 2 states. 0 for false, and 1 for true. This is fairly simple, and we have a lot of research and history in regards to how 0 and 1 can be used for scaling up to practically anything.
We already have precedent for how bits can be used to represent numbers, which in turn can be used to represent characters, strings, and much much more. We have learned to optimize for bits and binary logic in code, algorithms and so on. We have a lot of history with boolean algebra and logic gates as well.
Okay okay, so what even is a trit? We now know that a binary bit is a the smallest unit of data on a binary platform, so a trit must be the same, but for ternary? Yes. In ternary computing, the smallest unit of data is trit. A trit being a 3 state unit, with -1 / N (for negative), 0 / Z (for zero) and (+)1 / P (for positive).
There are a couple of standards for representing trits in a human readable format. With binary we’re used to 0 and 1, but with ternary, a precedent hasn’t been set yet. Some people use:
Unbalanced ternary:
0 0.5 1
0 / 1 (Sometimes can be another character than "/")
0 1 2
Balanced ternary:
- 0 +
-1 0 1
T 0 1
N Z P
For this project I will only use balanced ternary, this has been chosen for multiple reasons like it’s efficiency for signed Words, which also ties into Word arithmetics, and also for it fitting well with boolean logic, 0 still maps to False, 1 to True and we can keep -1 as a separate unknown / undefined state.
I personally like to use -1, 0 and 1 for more human readable and mathematical definitions, but I also use N, Z and P when it comes to representations of trits or Words in computing systems. Like for a representation of Word in a terminal, I would use NZP notation, while I’d use -1, 0 and 1 when describing it to a person or working with it more verbally.
So now, we have our definition for trits, also compared to bits:
So why even look into trits as an alternative to bits? If computing has gone well without ternary for so long, then there must be no reason to look into it. Right?
I can understand that sentiment, but I really don’t think it has to be so black and white. Yes, we have a lot of history and prior knowledge about binary in so many facets. Yet, there is still many cases looking into trits and ternary logic really makes sense. My personal favorites are in terms of ternary logic, Word arithmetics and radix efficiency.
There is also an argument that ternary could unlock the ability to get more performance in a post moore’s law computing world1.
The way a computer reads a 0 or a 1, is based on the voltage (V) that is delivered to the processor. For most common processors and logic gates, 0 will be 0V and 1 will be between 1V and 1,5V. You might be used to 1 being 5V for IC logic gates, but this is not standard for modern CPUs. For ternary, multiple voltage values have been suggested, Huaweis chip for for example uses CNTFETs, compared to CMOS, the standard of binary, and it uses 0V for 0, 1,65V for +1 and 3,3V for -12. I will hopefully have more to say on this topic ternary-logic.
Why go from 2 to 3 states? Wouldn’t it be more effective to go up to 8 or 16, or even 1024 states? That would make it much easier to address a lot of things.
The answer to this comes down to multiple things, like electrical complexity (specifically about voltage stability and thresholds for CMOS3), radix efficiency and wasted space. For a multitude of reasons, we’d prefer to keep it to fewer states, while optimizing for data it can represent.
For a start before going into specific cases of how arithmetics are performed on words in vildrose-cores code, I would like to make sure there’s a blanket understanding for binary and ternary arithmetics and how it relates to the decimal (base 10) we’re used to.
As an absolute baseline, it is important to understand what a base is, for this, we will start with an example in decimal. When counting in decimal, we go from 0 to 9, and once we reach 9, we loop over to 10. We can explain this as the one being in the 10s place. One way to understand this, is by representing it like this:
Decimal countingi * 10^0 (= 1)i * 10^1...i * 10^n
Here i is representing index / input and can be any natural number between 0 and 9 (0, 1, 2, 3, 4, 5, 6, 7, 8 or 9).
This might seem quite verbose compared to how we typically think about the decimal counting system, but this really is the most basic representation of it.
With this any decimal number can be deconstructed and understood. 25 for example, can be understand as 2 tens, and 5 ones.
But other counting systems also exist. You might already be familiar with binary (base 2) or hexadecimal (base 16):
Here i for binary can only be 0 or 1. And for hexadecimal i has to be between 0 and F (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A (10), B (11), C (12), D (13), E (14), F (15)).
With this you might see the pattern. The expression for counting can be expressed as such:
i * base^n = result
So for ternary, we can set up the same type of diagram as for binary:
Ternary countingi * 3^0 (= 1)i * 3^1...i * 3^n
In ternary, i can either be represented by 0, 1 or 2 for unbalanced ternary, or by using -1, 0 or 1 for balanced ternary (though I will use N, Z or P, since it better aligns to the code). Because of the benefits to arithmetics of balanced ternary, that is gonna be used for the examples here. Unbalanced ternary might not even be implemented, and instead only managed by software layers later on.
So let’s go through some examples of numbers in each of these base systems:
Decimal
Binary
Hexadecimal
Balanced Ternary
Unbalanced Ternary
0
0
0
Z
0
1
1
1
P
1
2
10
2
PN
2
3
11
3
PZ
10
5
101
5
PNP
12
8
1000
8
PNN
22
13
1101
D
PPP
111
25
11001
19
PZNP
221
At this point another pattern might appear, the smaller the base, the more digits are needed to represent a number. Yet we still don’t use hexadecimal in hardware, both because of complexity, but also because of radix efficiency.
But this leads to another question, what about negative numbers? In these cases, we use a “-” in front of the number, but this data also needs to be stored. Generally, it can be assumed, that if there is a chance for the type to be negative (e.g. it is balanced) we must have a separate piece of information to signal whether it is positive or negative. This is generally true for all types, except for Balanced Ternary.
Let’s go through a quick example for balanced ternary, let’s start by writing the number 33 and splitting it off into it’s separate parts:
33 = P (1 * 3^3 = 27) P (1 * 3^2 = 9) N (-1 * 3^1 = -3) Z (0 * 3^0 = 0)
33 = 27 + 9 + (-3) + 0
Okay, that looks nice and all, but what about a negative number? For that we can use the number -10 and do the same evaluation:
-10 = N (-1 * 3^2 = -9) Z (0 * 3^1 = 0) N (-1 * 3^0 = -1)
-10 = (-9) + 0 + (-1)
I won’t dance around it too much, but balanced ternary is really pretty for the way signing is incorporated directly into the type. And it opens up for a lot of easy integrations and arithmetics. A good example of this is how to read sign, absolute value, negation and more.
To check for a ternary words sign, it’s trivially easy. You just check what the sign of the first non zero trit is. This logic works, as the first non zero trit will always be the most significant trit. You can have a number like NPPPPP (-122) and it will still be true, since the first N, represents -243, which dwarfs all the other Ps.
Here’s the current code for how sign is extracted for words1:
/// Returns the sign (whether it's negative, positive or zero) of the word
pub fn sign(&self) -> Trit {
// For each t (trit) in own trit array (reversed to fit most to least significant logic)
for t in self.0.iter().rev() {
// If the checked trit isn't Z, then return and escape early
if *t != Trit::Z {
return *t;
}
}
// If none are found, return Z
Trit::Z
}
It also has the escape of returning zero, which another point of evidence for where trits, kleenes and ternary logic is so clean. Because if it had to return a boolean value, we’d be in trouble, then we’d have to decide whether 0 is positive or negative. Which isn’t a true understanding. Likely you’d return it as a positive value, but truly, it does more so represent a non value.
This is another situation where balanced ternary arithmetics are really clean. To negate a word in ternary, all you have to do is flip the sign of every trit in the word. This will leave you with the exact same number, just with the opposite sign. And for 0, nothing changes of course.
/// Returns the inverted (negated) form of the word
pub fn negate(&self) -> Self {
// For each trit in word, map it's trit to the negated form of the trit
Self(self.0.map(|t| t.negate()))
}
The .negate() function itself, is defined like this2:
/// Return the opposite (negated) for a trit
pub const fn negate(self) -> Self {
// Matches self (input) to find and return negated form
match self {
Self::N => Self::P,
Self::Z => Self::Z,
Self::P => Self::N,
}
}
Getting the absolute form of a word is as simple as checking whether the sign is negative, and if it is, you negate the whole word. This can be done really simply in code with an if statement, and referencing back to the negate function1:
/// Returns the absolute (no negatives) form of the word
pub fn abs(&self) -> Self {
// If the words sign is N
if self.sign() == Trit::N {
// then negate
self.negate()
} else {
// otherwise return itself
*self
}
}
Here protocols for MMIO devices, UART, framebuffer and so on will be defined. This will likely not be as in depth, and might focus on some experimental connections that are more ternary native, as otherwise, a binary core is way more suited for protocols already defined for binary.
In the future, the protocols, memory addresses and so on will be described in depth in this chapter.
The allocation of a trit in memory in vildrose core, is currently, using an i8 to represent a trit, and thus, the memory map is as follows:
Byte (1 trit)unusedunusedunusedunusedunusedunusedtrit datatrit data
This is quite wasteful. More than 75% of the memory currently goes unused, for this a couple of alternatives are being considered, such as packing 3 trits into a single byte, or even 4 trits into a single byte. The memory map for these alternatives is as follows:
Of these two paths, I more so lean towards the tribble approach. As a tribble with 3 trits, quite efficiently scales up into Tryte, Word9, Word27 and other types.
The real issue with packing trits into bytes, is that it makes the memory map more complex, and thus, the CPU will have to do more work to read and write trits from memory. This is a trade off that will have to be considered when deciding on the final memory map.
Tests for speed specifically should be setup, to see what maps best in regards to that.
I have foregone the idea of making my own ISA, as I am not nearly experienced enough to make decisions about such things, instead I will be building my VM on the idea of supporting a couple different ISAs. Some for ternary, some for binary, the idea with this is especially in regards to how logic can be shared or split between both types.