Software architecture as a tree

2022-05-12

In Rust, there are several ways to manage memory. Memory has two regions: one is called stack. Each new scope (between { and }) adds variables to the stack. Variables in the stack are reserved at once when a new scope is introduced and they are freed when the scope ends. This brings speed. Variables in the stack are also reserved contiguously, so they are more compatible to the principle of locality.

{
  var1
  var2
  { 
    var3
    var4
  }
}

The inner scope variables can't be accessed by the outer scope, but outer scope variables can be accessed by the inner scope. When we are in the inner scope the stack contains both scope's variables. Stack based memory management can be called table d'hote. It's faster and simpler.

However, in order to reserve them at once, variable sizes must be known beforehand. If the compiler doesn't know how long will an array be, it can't reserve the array in stack. We need another method to use this indefinite variables.

The other way of obtaining memory is using heap. Heap is independent of the scope. You can reserve and fill it when you need with a size determined in run time, share it with other scopes, and free it a la carte. Rust provides several different ways to manage the memory in heap.

In my experience, making the program flow as much as scoped without cross cutting memory management is a good sign. Ideally a program should have a tree like flow of execution and the number of heap allocations are minimized in this case. However, most of the modern software systems are not designed this way and memory management is not considered during the requirements. When you leave this to garbage collector, even if they do their best, it's usually haphazard.

Ideally, when you run the program it should run a few functions, which run a few functions, which run more functions:


flowchart LR

  main--->fn1 
  main--->fn2 
  main--->fn3 
  main--->fn4 
  fn1--->fn1.1
  fn1--->fn1.2
  fn1--->fn1.3
  fn1--->fn1.4
  fn2--->fn2.1
  fn2--->fn2.2
  fn2--->fn2.3
  fn2--->fn2.4

  fn1.1-->fn1.1.1
  fn1.1-->fn1.1.2
  fn1.1-->fn1.1.3
  fn1.1-->fn1.1.4
  fn1.1-->fn1.1.5


If we adhere to this principle, building concurrent software becomes a lot easier too. If functionality and memory of fn 1.3.1 and fn 2 are completely separate, these can be run in parallel. Hence our aim in software architecture must be looking for ways to represent the problem at hand into a tree structure, both physically (for the memory) and temporally (for execution.)