Easy to say but performance hit in reality (trade-offs): Don’t communicate by sharing memory, share memory by communicating. (Golang)
Rule of performance
Never guess about performance!
Measurements must be relevant
Lock-free algorithms do not always provide better performance,
due to it might fall into forever wait, e.g CAS.
Wait-free programs
Each thread will complete its task in finite number of steps (finite amount of time) no matter what other threads are doing. At all times, all threads make progress toward the final result “Step” is not the same as “time”
Lock-free programs
programs without locks (duh!); at least one thread makes progress no matter what other threads are doing.
Lock-based programs are not guaranteed to make progress toward the result in all cases.
In particular, consider what would happen if the thread holding the lock is waiting on another lock? That was held by the first thread?
Wait-free (or lock-free) does not mean “data-sharing-free”
Entire program is rarely wait-free or lock-free, focus on most performance-critical fragments.
"On the whole, I'm inclined to say that when in doubt, make a new type." – Martin Fowler, When to Make a Type
"Don't set a flag; set the data." – Leo Brodie, Thinking Forth
Considering this talk provides an essential abstraction design for Type.
Yet Golang's multiple variables return programming paradigm should design as follows the concept of considering them as a whole into sum type.
Types as sets of values:
Type, like math's function, defines value domain.
If types' value domain are same, we could consider they are equivalent.
(But not 'equality')
Algebraically, a type is the number of values that inhabit it.
e.g.
How many values?
bool; // 2, true, false
char; // 256
void; // 0
struct Foo {}; // 1
enum FireSwampDangers : int8_t { // 3
FLAME_SPURTS,
LIGHTNING_SAND,
ROUSES
};
template <typename T> // as many values as T
struct Foo {
T m_t;
};
Aggregating Types:
When two types are "concatenated" into one compound type,
we multiply the # of inhabitants of the components.
This kind of compounding gives us a product type.
e.g
How many values?
std::pair<char, bool>; // 256 * 2
struct Foo { // 256 * 2
char a;
bool b;
};
std::tuple<bool, bool, bool>; // 2 * 2 * 2 = 8
template <typename T, typename U> // (# of values in T) * (# of values in U)
struct Foo {
T m_t;
U m_u;
};
Alternating Types:
When two types are "alternated" into one compound type,
we add the # of inhabitants of the components.
This kind of compounding gives us a sum type.
e.g.
How many values?
std::optional<char>; // 256 + 1
std::variant<char, bool>; // 256 + 2
template <typename T, typename U> // (# of values in T) + (# of values in U)
struct Foo {
std::variant<T, U>;
}
Function Types:
The number of values of a function is the number of different ways we can
draw arrows between the inputs and the outputs.
When we have a function from A to B,
we raise the # of inhabitants of B to
the power of the # of inhabitants of A.
Curring, foundation of Lambda Calculus : https://en.wikipedia.org/wiki/Currying
e.g.
How many values?
bool f(bool); // 2^2 = 4
char f(bool); // 256 ^ 2
enum class Foo
{
BAR,
BAZ,
QUUX
};
char f(Foo); // 256 ^ 3
template <class T, class U> // U ^ T
U f(T);
The above definition gives us how to present equivalent type:
The addition of sum types to C++ offers an alternative formulation for some
design patterns.
State machines and expressions are naturally modeled with sum types.
Designing with types:
std::variant and std::optional are valuable tools that allow us to model
the state of our business logic more accurately. When you match the types to the domain accurately, certain categories of tests just disappear. (Consider Data Oriented Design)
Fitting types to their function more accurately makes code easier to
understand and removes pitfalls.
The bigger the code-base and the more vital the functionality, the more
value there is in correct representation with types.
Using types to constrain behavior:
"Phantom types" is one technique that helps us to model the behavior of
our business logic in the type system. Illegal behavior becomes a type error.
template <typename T, unsigned N>
class SmallVector : public SmallVectorImpl<T> {
typedef typename SmallVectorImpl<T>::U U; // expected-error {{no type named 'U' in 'SmallVectorImpl<CallSite>'}}
enum {
MinUs = (static_cast<unsigned int>(sizeof(T))*N + // expected-error {{invalid application of 'sizeof' to an incomplete type 'CallSite'}}
static_cast<unsigned int>(sizeof(U)) - 1) /
static_cast<unsigned int>(sizeof(U)),
NumInlineEltsElts = MinUs
};
U InlineElts[NumInlineEltsElts];
public:
SmallVector() : SmallVectorImpl<T>(NumInlineEltsElts) {
}
};
Small-size optimization is best when the values are small.
(in C++, copy by value is the default mechanism, although not being well/widely known...) Design:
Give large objects address identity. i.e Use object's memory address as identity avoids object's content equality test.
Note: Or, if it's pure, consider writing it as callable object, which introduce a layer of 'namespace' and take advantage of EBO(Empty base class optimizations), [no_unique_address], refer to: [C++][accu2018] Tricks library implementation to know), i.e no constructor call overhead, class as a layer of namespace.
What’s a Data Type?
A data type is a bundle of compile-time properties for an object:
• size and alignment
• set of valid values
• set of permitted operations “If you’re arguing, you’re losing.”