Showing posts with label design_dod. Show all posts
Showing posts with label design_dod. Show all posts

Dec 1, 2024

[C++][cpponsea 2024] C++ Cache Friendly Data + Functional + Ranges summary

Reference:
https://www.youtube.com/watch?v=XJzs4kC9d-Y
https://github.com/rollbear/columnist

https://godbolt.org/z/a8P7oTjoh

#include <functional>
#include <iostream>
#include <tuple>
#include <vector>

template<typename... Ts>
class Table {
 public:
  using row = std::tuple<Ts&...>;
  row operator[](size_t i) {
    auto access = [&]<size_t... Is>(std::index_sequence<Is...>) {
      return row{std::get<Is>(data_)[i]...};
    };
    return std::invoke(access, indexes);
  }

  Table() {
    init(indexes);
  }

 private:
  static constexpr std::index_sequence_for<Ts...> indexes{};
  std::tuple<std::vector<Ts>...> data_;

 private:
  template<size_t... I>
  constexpr void init(std::index_sequence<I...>) {
    ((std::get<I>(data_).push_back({42}),...));
  }
};

struct Data1 {
 int value;
};

struct Data2 {
 int value;
};

int main() {

  Table<Data1, Data2> table;
  auto row = table[0];
  std::cout << std::get<0>(row).value << '\n'; // print 42
};


Iterator

Use sentinal pattern.
struct sentinel {}; // or using sentinal = void;

struct iterator {
  using value_type = row;
  using difference_type = ssize_t;

  value_type operator*() const {
    return std::invoke([&]<size_t... Is>(std::index_sequence<Is...>) {
      return value_type{std::get<Is>(t->data_)[offset]...};
    }, t->indexes);
  }

  iterator& operator++() { ++offset; return *this;}
  iterator operator++(int) { auto copy = *this; ++*this; return copy;}
  bool operator==(const iterator&) const = default;
  bool operator==(sentinel) const { return offset == t->size(); }
  table* t;
  size_t offset;
};

Plug in
template<typename... Ts>
class Table {
 public:
  friend class iterator;
  iterator begin() { return {this, 0}; }
  sentinel end() { return {}; }
...
};

Reference:
https://en.cppreference.com/w/cpp/utility/integer_sequence
template<typename...>
struct Table;

template<typename, typename>
struct Row;


template <typename... Ts, size_t... Cs>
struct Row<Table<Ts...>, std::index_sequence<Cs...>> {
  using row_id = typename Table<Ts...>::row_id;

  // Indirection to make std::index_sequence<0, 2, 4> -> I==1 means std::get<2>(...)
  template<size_t I>
  friend auto& get(const Row& r) {
    static constexpr std::array columns{Cs...};
    return std::get<columns[I]>(r.t->data_)[r.offset];
  }

  ...
  Table<Ts...>* t;
  size_t offset;
};


template<typename Table, size_t... Cs>
struct std::tuple_size<Row<Table, std::index_sequence<Cs...>>>
 : std::integral_constant<size_t, sizeof...(Cs)> {};


template <size_t I, typename... Ts, size_t... Cs>
struct std::tuple_element<I, Row<Table<Ts...>, std::index_sequence<Cs...>>> {
  static constexpr std::array indexes = { Cs... };
  using type = Ts...[indexes[I]]&;
};

Select:
template<size_t... Is, typename Table, size_t... Cs>
auto select(const Row<Table, std::index_sequence<Cs...>>& r) {
  static constexpr size_t columns[] { Cs... };
  return Row<Table, std::index_sequence<columns[Is]...>>(r);
}

Usage:
drop_if(values, [](auto r){ auto [x,z] = select<0, 2>(r); return x < z; });
template<typename R, size_t... Cs>
struct range_selector {
  using r_iterator = decltype(std::declval<R&>().begin());
  struct iterator : r_iterator {
    using difference_type = ssize_t;
    using value_type = decltype(select<Cs...>(*std::declval<r_iterator>()));
    
    iterator(const r_iterator& i) : r_iterator(i) {}
    auto operator*() const {
      const r_iterator& i = *this;
      return select<Cs...>(*i);
    }
  };

  iterator begin() { return iterator{ r.begin() }; }
  auto end() { return r.end(); }

  R& r;
};
template<size_t... Cs>
struct range_selector_maker {
  template<typename R>
  friend range_selector<R, Cs...> operator|(R& r, range_selector_maker) {
    return { r };
  }
};

template<size_t... Cs>
range_selector_maker<Cs...> select() { return {}; }

Usage:
for (auto [x, d] : values | select<0, 3>()) {
  std::println("d={} x={}", d, x);
}

Concepts:
template<typename>
inline constexpr bool row_type_v = false;

template<typename T, typename Cs>
inline constexpr bool row_type_v<Row<T, Cs>> = true;

template<typename T>
concept row_type = row_type_v<T>;

Select:
template<size_t... Cs, typename F>
auto select(F&& f) 
  requires (! row_type<std::remove_cvref_t<F>>)
{
  return [f = std::forward<F>(f)]<typename T, typename Is>(row<T, Is> r) {
    return f(select<Cs...>(r));
  };

}
template<typename F>
auto apply(F&& f) {
  return [f = std::forward<F>(f)]<row_type R>(R r) {
    return std::invoke([&]<size_t... Cs>(std::index_sequence<Cs...>) {
      return f(get<Cs>(r)...);
    }, std::make_index_sequence<std::tuple_size_v<R>>{});
  };
}


Usage:
drop_if(values, select<0, 2>(apply([](auto x, auto z) { return x < z; })));

or just:
drop_if(values, select<0, 2>(apply(std::less{})));

Jul 4, 2020

[Pacific++ 2018][re-read] "Designing for Efficient Cache Usage" - Scott McMillan

Reference:
Latency Numbers Every Programmer Should Know:
https://colin-scott.github.io/personal_website/research/interactive_latency.html



Cache Lines



 

Hardware Prefetch

  • Predictable access patterns are faster
  • We want sequential locality

 

Access Locality

  • Cache locality
    • Spatial
    • Temporal
  • Beware the algorithm/data structure to use honors cache locality.
  • Look beyond just big-O notation as constant-time costs can differ significantly.
  • Large benefit in hitting faster cache levels.
  • In C++, allocators matters. In Go(lang), the standard implement dealt with this problem.
  • In Go(lang), use flat map instead of std map.

 
 

Multipe CPU Core Considerations



 
 

Write Combined Memory

  • Accumulate writes to flush as 64Bytes  operations
  • Partial buffer flush causes: (avoid this)
    • Not writing all bytes convered by a buffer
    • Writing too many streams at once
    • Atomic read-modify-write operations
  • Write Combined memory read causes:
    • C++ bit fields
    • Optimization
    • Virtually always an accident (read the source code of implement)
    • Solution: Expose write-only interface
  • Non-temporal writes on x86: 
    Optimizing Cache Usage With Nontemporal Accesses
    • Use compiler intrinsics:
      • SSE2
        • _mm_stream_si32: store 4 bytes
        • _mm_Stream_si128: store 16 bytes
      • AVX
        • _mm256_stream_si256: store 32 bytes
      • AVX-512
        • _mm512_stream_si512: store 64 bytes



Address Translation

  • Platform-specific
  • Not directly pageable
  • Difficult/slow to allocate
  • Linux: Use hugepage

Jan 5, 2019

[Design][Software Engineering][C++] Using/Design type effectively - Ben Deane@Blizzard

"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:

e.g.
Equivalence:
template <typename T>
struct Foo {
    std::variant<T, T> m_v;
};
template <typename T>
struct Bar {
    T m_t;
    bool m_b;
};


Algebraic Datatypes:

  • the ability to reason about equality of types
  • to find equivalent formulations
    • more natural
    • more easily understood
    • more efficient
  • to identify mismatches between state spaces and the types used to
    implement them
  • to eliminate illegal states by making them inexpressible


Making illegal states unrepresentable:

std::variant is a game changer because it allows us to (more) properly express types,
so that (more) illegal states are un-representable.

Let's using sum types (variant, optional) as well as product types (structs):
e.g
Old way:
enum class ConnectionState {
    DISCONNECTED,
    CONNECTING,
    CONNECTED,
    CONNECTION_INTERRUPTED
};

struct Connection {
    ConnectionState m_connectionState;
    std::string m_serverAddress;
    ConnectionId m_id;
    std::chrono::system_clock::time_point m_connectedTime;
    std::chrono::milliseconds m_lastPingTime;
    Timer m_reconnectTimer;
};

New way:
struct Connection {
    std::string m_serverAddress;

    struct Disconnected {};
    struct Connecting {};
    struct Connected {
        ConnectionId m_id;
        std::chrono::system_clock::time_point m_connectedTime;
        std::optional<std::chrono::milliseconds> m_lastPingTime;};

    struct ConnectionInterrupted {
        std::chrono::system_clock::time_point m_disconnectedTime;
        Timer m_reconnectTimer;};

    std::variant<Disconnected,
      Connecting,
      Connected,
      ConnectionInterrupted> m_connection;
};

Old way:
class Friend {
std::string m_alias;
bool m_aliasPopulated;
...
};

New way:
class Friend {
std::optional<std::string> m_alias;
...
};


Thus, we have a new design pattern for modern C++:

  • Command
  • Composite
  • State
  • Interpreter
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.
e.g.
Old ways:
std::string GetFormData();
std::string SanitizeFormData(const std::string&);
void ExecuteQuery(const std::string&);

template <typename T>
struct FormData {
    explicit FormData(const string& input) : m_input(input) {}
    std::string m_input;
};
struct sanitized {};
struct unsanitized {};

New ways:
FormData<unsanitized> GetFormData();

std::optional<FormData<sanitized>>
SanitizeFormData(const FormData<unsanitized>&);

void ExecuteQuery(const FormData<sanitized>&);


Total functions:

  • A total function is a function that is defined for all inputs in its domain.
  • Writing total functions with well-typed signatures can tell us a lot about functionality.
  • Using types appropriately makes interfaces unsurprising, safer to use and harder to misuse.
  • Total functions make more test categories vanish.
  • Effectively using types can reduce test code.


Name this function:

(having lambda calculus knowledge is essential to understand what's going on next)
template <typename T>
T f(T);
// identity
// int f(int);

template <typename T, typename U>
T f(pair<T, U>);
// first

template <typename T>
T f(bool, T, T);
// select

template <typename T, typename U>
U f(function<U(T)>, T);
// apply or call

template <typename T>
vector<T> f(vector<T>);
// reverse, shuffle, ...

template <typename T>
optional<T> f(vector<T>);

template <typename T, typename U>
vector<U> f(function<U(T)>, vector<T>);
// transform

template <typename T>
vector<T> f(function<bool(T)>, vector<T>);
// remove_if, partition, ...

template <typename K, typename V>
optional<V> f(map<K, V>, K);
// lookup

template <typename T>
T f(vector<T>);
// Not possible! It's a partial function - the vector might be empty.
// T& vector<T>::front();

template <typename T>
T f(optional<T>);
// Not possible!

template <typename K, typename V>
V f(map<K, V>, K);
// Not possible! (The key might not be in the map.)
// V& map<K, V>::operator[](const K&);


Take away:

  • Make illegal states unrepresentable
  • Use std::variant and std::optional for formulations that are
    • more natural
    • fit the business logic state better
  • Use phantom types for safety
    • Make illegal behavior a compile error
  • Write total functions
    • Unsurprising behavior
    • Easy to use, hard to misuse


Reference:

[golang][c++] padding https://vsdmars.blogspot.com/2018/09/golangc-padding.html

Nov 14, 2018

[Cppcon 2016] High Performance Code 201: Hybrid Data Structures - Chandler Carruth


std::vector's problem, no SSO(small size optimization)
Why? Because STD says when move a vector, it's iterator can't be invalidated.
That implies std::vector's iterator as pointer points to heap memory.
However, SSO vector when moves, it copies, and invalidates the iterators.

Domain specific data structure has it's purpose of efficiency.
(Less corner cases, easier to design.)

Besides domain specific data structure,
why not just using customized allocator with std::vector?

Thus SSO for vector becomes:
template<typename T, int N>
using SmallVector = std::vector<T, short_alloc<T, N>>;

void fun(){
    SmallVector<int, 4>::allocator_type::arena_type a;
    SmallVector<int> v{a};
}

But...
It doesn't work well with interface boundary,
i.e  due to std::vector has allocator as type argument.
void jump(SmallVector<int, 4> &v);

void fun(){
    // Taking short_alloc<int, 8>
    SmallVector<int, 8>::allocator_type::arena_type a;
    SmallVector<int> v{a};
    jump(v);  // doesn't work... callee taking short_alloc<int, 4>
}

Another issue, callee's return type could reference to memory on callee's stack..
i.e
SmallVector<int, 4> fun(){
    SmallVector<int, 4>::allocator_type::arena_type a;
    SmallVector<int> v{a};
    return v; // BAD
}

All of all, the SmallVector loses it's value semantics.
Which is IMPORTANT.

With Domain Specific Type, these issues solved.
SmallVector type in Clang:
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:
  1. Give large objects address identity.
    i.e Use object's memory address as identity avoids object's content equality test.
  2. SmallVector<std::unique_ptr<BigObject>, 4> Objects;
    
    BumpPtrAllocator impl, purpose, make BigObject as compact as possible on heap memory.
    // FAST
    class BumpPtrAllocator {
        constexpr int SlabSize = 4096;
        SmallVector<void *, 4> Slabs;
        void *CurPtr, *End;
    
    public:
        void *Allocate(int Size) P
            if (Size >= (End - CurPtr)) {
                CurPtr = malloc(SlabSize);
                End = CurPtr + SlabSize;
                Slabs.push_back(CurPtr);
            }
    
            void *Ptr = CurPtr;
            CurPtr += Size;
            return Ptr;
        }
        // ...
    };
    
  3. If pointers are too large, use an index.
  4. Aggressively pack the bits.
    PointerIntPair:
    http://llvm.org/doxygen/classllvm_1_1PointerIntPair.html
    PointerEmbeddedInt:
    http://llvm.org/doxygen/classllvm_1_1PointerEmbeddedInt.html
    TinyPtrVector:
    http://llvm.org/doxygen/classllvm_1_1TinyPtrVector.html
    Thus, we have SmallMutiMap:
  5. template<typename KeyT, typename ValueT>
    using SmallMultiMap = SmallDenseMap<KeyT, TinyPtrVector<ValueT>>;
    
  6. Use bitfields everywhere.
  7. Sometimes, we need an ordering.
    i.e comparison operator.
  8. Where possible, sort the vector.
    i.e gives you a linear BST, works well with CPU pre-fetching.
  9. What if there's no intrinsic ordering?
    We have SmallSetVector:
    (Has a set, has a vector, when insert, check data in set, and insert into vector.)
    http://llvm.org/doxygen/classllvm_1_1SmallSetVector.html

[Pacific++ 2018] "Designing for Efficient Cache Usage" - Scott McMillan



Categorized into 6 sections
  • Cache Lines
  • Hardware Prefetch
  • Access Locality
  • Multiple CPU Core consideration
  • Write Combined Memory
  • Address Translation

This talk can be a supplement to 'OOP Is Dead, Long Live Data-oriented Design - Stoyan Nikolov' talk.



Cache Lines
Transfer occur as cache lines.
(Think about Data Oriented Design)
reference:


Hardware Prefetch
Predictable access patterns are faster.
We need sequential locality.



Access Locality
Cache locality
  • spatial
  • temporal
Use vector.

Hash map with key designed being flat.
reference:
Paper:


Multiple CPU Core consideration
(MESI)


Write Combined Memory
Use compiler intrinsics:
  • SSE2
    • _mm_stream_si32: store 4 bytes
    • _mm_Stream_si128: store 16 bytes
  • AVX
    • _mm256_stream_si256: store 32 bytes
  • AVX-512
    • _mm512_stream_si512: store 64 bytes


Address Translation
TLB Size (4KiB pages)
  • Address translation can be a significant overhead.
  • Large pages can help.
Linux
  • Huge TLB Page
    • Allocate on hugetlbfs
    • Access via mmap or shared memory
  • Transparent Huge Pages
    • Latency spike bewared

Nov 2, 2018

[cppcon 2018] OOP Is Dead, Long Live Data-oriented Design - Stoyan Nikolov


Data-Oiented Design

OOP marries data with operations

  • Heterogeneous data is brought together by a 'logical' black box object.
  • The object is used in vastly different contexts
  • Hides 'state' all over the place
  • Impact on
    • Performance
    • Scalability
    • Modifiability
    • Testability
  • Why? Cache miss~

Data-oriented design

  • Like Golang, data first
  • Separates data from logic
  • Structs and functions live independent lives
  • Data is regarded as information that has to be transformed
  • The logic embraces the data
  • Does not try to hide the logic
  • Leads to functions that work on arrays
  • Reorganizes data according to it's usage

If we aren't going to use a piece of information, why packs it together?

Examples from Chromium code base :-)

--
class CORE_EXPORT Animation final: public ~
--


So, for OOP in Chromium:
  • Uses more than 6 non-trivial classes
  • Objects contain smart pointers to other objects
  • Interpolation uses abstract classes to handle different property types
  • CSS Animations directly 'reach out' to other systems - coupling
  • Calling events
  • Setting values in DOM element
  • What's the lifetime of elements being synchronized?



DOD:
  • Data operations
    • Tick -> 99.9%
    • Add
    • Remove
    • Pause
    • ...
  • Tick Input
    • Definition
    • Time
  • Tick Output
    • Changed properties
    • New property values
    • Who owns the new values
  • Design for 'many animations',
    i.e many objects


Define a type:
struct AnimationController{
    AnimationState* as_ [];
};

// Golang style.
// No shared_ptr, every instance of this type
// has it's own value. 
// Thread safe.
struct AnimationState{
    AnimationID Id;
    time StartTime;
    time PauseTime;
    ...
};

// Avoid type erasure, use template
template<typename T>
struct AnimationStateProperty : public AnimationState {
    AnimatedDefiniationFrames<T> Keyframes;
};


// We can't use vector<baseType>
// But since we know every property types,
// create vector for each type
CSSVector<AnimationStateProperty<ZIndex>> m_ZIndexActiveAnimState;

// Iterates them for every CSSVector types

With above design, keep in mind,
std::vector
is the best container to avoid cache misses!
(continuous memory, sequential container)



Avoid branches:
  • Keep lists per-boolean 'flag'
  • Separate Active and Inactive animations
    i.e Base on the states we have, put object into a list of the same state.
  • avoid using 'if branch' test.
  • Avoid 'if (isActive)'
  • If there are too many states, try to cut down the size of states, or put the state that changes most into 'list' style.



Add API to the caller:
  • We don't have OOP style object, thus
    no member functions!
    i.e Animation.Play()
  • Use free function taking ID!
    i.e
    void PlayAnimation(AnimationID aid);


Key points:
  • Keep data flat (Golang style)
    • Maximise cache usage
    • No RTTI
    • Amortized dynamic allocations
    • Some read-only duplication improves performance and readability
  • Existence-based predication
    • Reduce branching
    • Apply the same operation on a whole table
  • Id-Based handles
    • No pointers
    • Allow rearranging internal memory
  • Table-based output
    • No external dependencies
    • Easy to reason about the flow


Scalability:
  • OOP multi-threading
    • Complicated
  • DoD multi-threading
    • Group state into list
    • Each task/job/thread keeps a private table of modified data
    • Join merges the tables (thread.join)
    • Classic fork-join


Testability:
  • OOP case
    • Hard to mock(lots of types)
    • Hidden states
    • Asserting correct state is difficult - multiple output points(VERY BAD DESIGN)
  • DOD case
    • Contract style design
    • Easier to mock(less types)
    • Asserting correct state is easy

    
Modifiability:
  • OOP
    • Hard to modify base types
    • But, easy to do 'quick' changes, because we have if branches
  • DOD
    • FP style. Building blocks
    • A bit harder to to quick changes, but with FP, we have monoid.

    
Downsides of DOD:
  • Correct data separation can be hard
    • Know the problem well
  • Existence-based predication is not always feasible(or easy)
  • 'Quick' modifications can be tough


What to keep from OOP:
  • Simple struct with simple methods are fine
  • Keep polymorphism & interface under control
  • Use template
  • Use 'impl'


Extra reference:

Nov 19, 2017

[CPPCON 2014] Data-oriented Design - Mike Acton

video:
https://www.youtube.com/watch?v=rX0ItVEVjHc

  • No Exception
  • No Template
  • No IOSTREAM
  • No Multiple Inheritance
  • Seldom Operator overloading
  • No RTTI (turn off) i.e no virtual function
  • No STL
  • Custom allocators
  • Custom debugging tools (into the code)


The purpose of all programs, and all parts of those programs, is to transform data from one form to another.

If you don't understand the data you don't understand the problem.

Conversely, understand the problem by understanding the data.

Different problems require different solutions.

If you don't understand the cost of solving the problem, you don't understand the problem.

If you don't understand the hardware, you can't reason about the cost of solving the problem.

Everything is a data problem. Including usability, maintenance, debug-ability etc.
It's not code problem.

Avoid adding problem into the problem while solving the problem.

Latency and throughput are only the same in sequential systems.

Rule of thumb: Where there is one, there are many. Try looking on the time axis.

Code is NEVER more important then data.

Software is NOT platform. Hardware is.

There's no ideal abstract solution.

Help the compiler reasoning.

Don't put evaluation inside a loop. Put it outside.

Good programming is HARD.
Bad programming is EASY.

Design pattersn are spoonfeed material for brainless programmers incapable of independent thought, who will be resolved to producing code as mediocre as the design patterns they use to create it.