Systems programming asks learners to reason across several layers at once. Some of those layers are visible in the source code, but many of the behaviours that matter emerge only after the language rules, compiler, runtime environment, operating system, and hardware interact. That does not explain every difficulty in learning systems programming, but it does create a recurring problem: the model in the learner’s head can be difficult to compare with what the machine is actually doing.
A loop can be syntactically simple while its observed performance depends on data layout, access pattern, branch behaviour, vectorisation, allocation, contention, and the code the compiler emits. Pointers provide another example. On mainstream implementations a pointer value is closely related to an address, but C++ pointer operations are governed by language rules about objects, lifetimes, types, and valid access. “A pointer is just an address” can be a useful first approximation, but it becomes misleading if the approximation is never refined.

Hidden behaviour makes reasoning harder
When beginners struggle with systems concepts, it is tempting to conclude that the material is simply too advanced. Computing education research suggests a more useful explanation. Learners form mental models of how programs execute, and those models can be productive or misleading. In a study of novice programmers, Ma et al. (2011) found that students whose models of assignment and references were more viable also performed better on programming tasks.
That study did not investigate caches, C++, or operating systems, so its result should not be promoted into direct evidence about systems programming. The narrower point is still useful: the explanatory model a learner uses can affect how they reason about code. Sorva (2013) provides a stronger conceptual bridge through the idea of a notional machine, an abstract account of how programs execute. His review argues that runtime dynamics and the machine implied by a programming language deserve explicit attention rather than being left entirely for students to infer.
That makes notional machines relevant to systems teaching, but it is still a conceptual bridge. The introductory-programming literature does not establish that a particular way of exposing caches, memory layout, or compiler behaviour will improve systems-programming learning. That remains an empirical question.

The important step for learning is the return path. Observation gives us evidence with which to revise the model.
Visualisation can help, but showing more is not automatically better
Program visualisation has a long history in computing education. Sorva, Karavirta, and Malmi (2013) reviewed generic program-visualisation systems for introductory programming. Their synthesis found that evaluation results largely supported the use of program visualisation, but they also stressed that the evidence was too limited to make confident, fine-grained claims about which kinds of learner engagement work best.
That caution matters. Earlier, Hundhausen, Douglas, and Stasko (2002) synthesised 24 experimental studies of algorithm visualisation and found that successful studies were associated more consistently with cognitively active use of visualisations than with representational features alone. A later meta-analysis of programming education by Scherer, Siddiq, and Sánchez Viveros (2020), covering 139 interventions and 375 effect sizes, estimated an average effect for visualisation interventions of Hedges’ g = 0.44, 95% CI [0.29, 0.58]. That supports a positive average effect across the included studies. It does not show that any particular visualisation, representation, or interaction will improve learning in every context.
The implication is not that every low-level concept needs an animation. A representation should earn its place by helping answer a specific question. If the learner is investigating locality, show access order and measured behaviour. If the question is object lifetime, show construction, storage, and destruction. If the question is contention, expose the shared state and coherence activity that the source code alone does not reveal.
Cognitive load is a constraint, not a reason to hide the machine
Systems material often requires learners to coordinate several new ideas at once, so cognitive load is a legitimate instructional-design concern. In their systematic review of programming education, Berssanette and de Francisco (2022) found that subjective cognitive-load measurement and the worked-example effect were among the most common CLT-related concepts in the literature they reviewed. Duran, Zavgorodniaia, and Sorva (2022) found a different problem: computing-education papers often invoked cognitive load theory only briefly, rarely formulated hypotheses in terms of specific load components, and used a limited set of measures. That makes “reduce cognitive load” too vague to justify an instructional choice by itself.
A more defensible principle is selective visibility: expose the behaviour needed to answer the learner’s current question, and suppress detail that is not yet useful. The simplification should be explicit, so the learner knows where the model stops rather than mistaking it for the whole machine.
Three concrete learning examples
The pedagogical argument becomes clearer when it is expressed as tasks rather than principles. Each of the following starts with a prediction, exposes one hidden mechanism, and then asks the learner to revise the model.
Example 1: Why is this object larger than its fields?
#include <cstddef>
#include <iostream>
struct Record {
char tag;
int value;
char flag;
};
int main() {
std::cout << sizeof(Record) << '\n';
std::cout << offsetof(Record, tag) << ' '
<< offsetof(Record, value) << ' '
<< offsetof(Record, flag) << '\n';
}
Prediction: ask the learner to estimate the object size from the members. Observation: compile the program and inspect sizeof and the member offsets. Refinement: introduce alignment and padding when the observed layout differs from the naive sum. The important lesson is not a particular byte count, which is implementation-dependent. It is that source declarations constrain object representation without uniquely determining every detail of that representation.
Example 2: Two correct loops, different memory behaviour
Use the sequential-versus-shuffled experiment below. Before running it, require a written prediction about both correctness and timing. After running it, ask which parts of the explanation are directly observed and which remain hypotheses about caches or prefetching. If perf is available, compare hardware counters with the timing result rather than treating elapsed time as the whole explanation.
Example 3: Two variables that still interfere
Give two threads separate atomic counters and ask whether the counters can affect each other’s performance. Compare adjacent counters with a layout that deliberately separates them. In C++17 and later, std::hardware_destructive_interference_size provides an implementation-defined value intended to help avoid destructive interference, but it should be treated as a platform-specific hint rather than a universal cache-line guarantee. The learner begins with a source-level model in which the variables are logically independent, then asks whether their physical placement can still create coherence traffic.
This example is useful because both versions can be functionally correct. The question is not whether one contains a programming error, but whether the physical layout creates additional coherence work under the tested workload. A fuller false-sharing experiment belongs in the companion performance-education article, where the hardware and measurement assumptions can be stated explicitly.
Evidence from interactive memory visualisation
A recent study provides a useful example of active rather than passive visualisation. Pham, Nguyen, and Schreib (2024) evaluated MemStep, a Java tool that asks students to identify memory operations as execution progresses and then updates a stack-and-heap representation when the operation is correct. In a controlled evaluation within a sophomore program-design course, the authors reported that MemStep was at least as effective as the comparison learning activities and more engaging. The study is small in scope, course-specific, and Java-specific, so it is not evidence that the same design will improve C++ systems education. It is a concrete example of an instructional pattern that could be adapted and evaluated: prediction, action, feedback, and model revision.
Runnable experiment: same data, different access order
The following experiment isolates access order more cleanly than comparing two different container types. Both timed loops use the same indexed operation, visit the same values exactly once, and compute the same sum. The only intentional difference is the order stored in the index vector: one is sequential and the other is deterministically shuffled.
#include <algorithm>
#include <chrono>
#include <cstdint>
#include <iostream>
#include <numeric>
#include <random>
#include <vector>
using clock_type = std::chrono::steady_clock;
template <class F>
double time_ms(F&& f) {
const auto start = clock_type::now();
f();
const auto stop = clock_type::now();
return std::chrono::duration<double, std::milli>(
stop - start
).count();
}
int main() {
constexpr std::size_t n = 1u << 22;
std::vector<std::uint32_t> values(n);
std::iota(values.begin(), values.end(), 1u);
std::vector<std::uint32_t> sequential_order(n);
std::iota(sequential_order.begin(), sequential_order.end(), 0u);
auto shuffled_order = sequential_order;
std::mt19937 rng(42);
std::shuffle(shuffled_order.begin(), shuffled_order.end(), rng);
std::uint64_t sequential_sum = 0;
const double sequential_ms = time_ms([&] {
for (const auto index : sequential_order) {
sequential_sum += values[index];
}
});
std::uint64_t shuffled_sum = 0;
const double shuffled_ms = time_ms([&] {
for (const auto index : shuffled_order) {
shuffled_sum += values[index];
}
});
std::cout << "sequential order: " << sequential_ms
<< " ms, sum=" << sequential_sum << '\n';
std::cout << "shuffled order: " << shuffled_ms
<< " ms, sum=" << shuffled_sum << '\n';
}
Build and run
g++ -std=c++20 -O2 -march=native hidden_state.cpp -o hidden_state
./hidden_state
# Or with Clang
clang++ -std=c++20 -O2 -march=native hidden_state.cpp -o hidden_state
./hidden_state
The exact timings are machine- and workload-dependent. On many contemporary processors, sequential indices will produce a faster traversal because consecutive accesses to values are more favourable to spatial locality and often to hardware prefetching. A shuffled order makes those accesses less regular. That explanation is plausible, but the timing alone does not prove which microarchitectural mechanisms were responsible. Research on cache-conscious data organisation provides independent evidence that layout and locality can materially affect cache behaviour and application performance in the workloads studied (Chilimbi, Hill, & Larus, 1999).
This program is a teaching experiment, not a publication-quality benchmark. A single timing cannot establish a portable effect size, and elapsed time alone cannot identify the microarchitectural cause of a difference. For research use, run repeated trials, report the distribution, record the hardware and compiler configuration, account for order and system-load effects, and define the analysis protocol before looking for the preferred result.
Do not turn this into another rule
The experiment does not establish that sequential access is always fast or that shuffled access is always slow. Dataset size, cache hierarchy, address translation, compiler transformations, prefetchers, memory technology, processor architecture, and competing system activity can all affect the result.
A better conclusion is: access order is a variable worth measuring.
Make the experiment reproducible
If you want to compare results with somebody else, record enough context to explain the run:
g++ --version
clang++ --version
uname -a
# Linux only, if perf is available
perf stat -d ./hidden_state
On Linux, perf stat can collect hardware performance-counter statistics. Event availability and semantics depend on the processor and performance-monitoring unit, so counters need to be reported with the hardware and event configuration. Treat them as measurements to interpret, not labels that explain a program automatically.
Validation note
Before publication, both C++ examples were compiled and run on Debian GNU/Linux 13 on x86-64 using GCC 14.2 and Clang 17. The Record example produced a 12-byte object with member offsets 0, 4, and 8 in both builds. The access-order experiment produced identical sums for the sequential and shuffled traversals in both builds. The shuffled traversal was slower in this environment, but the timing ratio is deliberately omitted because a single machine is not evidence for a portable effect size.
What this changes about teaching systems programming
The evidence reviewed here does not justify beginning an introductory lesson with cache-coherence protocols or the full C++ object model. That would add detail before the learner has a question that needs it. A more plausible design principle is progressive visibility: begin with a useful model, then expose additional mechanisms when an observation makes the limitation of that model relevant.
- Start with a useful model. Keep only the concepts required for the immediate task.
- Make a prediction. Ask what the learner expects the program to do and why.
- Expose one hidden behaviour. Use a debugger, profiler, sanitizer, visualisation, or small experiment.
- Compare prediction with observation. A mismatch is a learning opportunity, not a failure.
- Refine the model. Add the minimum new detail needed to explain the evidence.
This is the hypothesis behind my Visible Systems work, not a conclusion the literature has already established for systems programming. The research question is whether making carefully selected system behaviour observable can improve the accuracy, calibration, or transfer of learners’ reasoning compared with other forms of instruction. That needs to be tested directly.
Where this article fits
This article makes the broad educational case for observable systems. For a concrete technical example, see What Actually Happens When Your C++ Code Touches Memory. For the empirical questions behind the approach, see the Research section.
References
- Berssanette, J. H., & de Francisco, A. C. (2022). Cognitive load theory in the context of teaching and learning computer programming: A systematic literature review. IEEE Transactions on Education, 65(3), 440–449. https://doi.org/10.1109/TE.2021.3127215
- Chilimbi, T. M., Hill, M. D., & Larus, J. R. (1999). Cache-conscious structure layout. In Proceedings of the ACM SIGPLAN 1999 Conference on Programming Language Design and Implementation (PLDI ’99) (pp. 1–12). Association for Computing Machinery. https://doi.org/10.1145/301618.301633
- Duran, R., Zavgorodniaia, A., & Sorva, J. (2022). Cognitive load theory in computing education research: A review. ACM Transactions on Computing Education, 22(4), Article 40, 1–27. https://doi.org/10.1145/3483843
- Hundhausen, C. D., Douglas, S. A., & Stasko, J. T. (2002). A meta-study of algorithm visualization effectiveness. Journal of Visual Languages & Computing, 13(3), 259–290. https://doi.org/10.1006/jvlc.2002.0237
- Ma, L., Ferguson, J., Roper, M., & Wood, M. (2011). Investigating and improving the models of programming concepts held by novice programmers. Computer Science Education, 21(1), 57–80. https://doi.org/10.1080/08993408.2011.554722
- Pham, M. L., Nguyen, A., & Schreib, R. (2024). MemStep: An interactive tool for constructing and visualizing the run-time memory layout of Java programs. In Proceedings of the 2024 on Innovation and Technology in Computer Science Education V. 1 (ITiCSE 2024) (pp. 541–547). Association for Computing Machinery. https://doi.org/10.1145/3649217.3653532
- Scherer, R., Siddiq, F., & Sánchez Viveros, B. (2020). A meta-analysis of teaching and learning computer programming: Effective instructional approaches and conditions. Computers in Human Behavior, 109, 106349. https://doi.org/10.1016/j.chb.2020.106349
- Sorva, J. (2013). Notional machines and introductory programming education. ACM Transactions on Computing Education, 13(2), Article 8, 1–31. https://doi.org/10.1145/2483710.2483713
- Sorva, J., Karavirta, V., & Malmi, L. (2013). A review of generic program visualization systems for introductory programming education. ACM Transactions on Computing Education, 13(4), Article 15, 1–64. https://doi.org/10.1145/2490822
Technical references
- ISO/IEC. (2024). ISO/IEC 14882:2024: Programming languages: C++. International Organization for Standardization. https://www.iso.org/standard/83626.html
- cppreference.com. (n.d.). std::hardware_destructive_interference_size, std::hardware_constructive_interference_size. Retrieved October 5, 2026, from https://en.cppreference.com/w/cpp/thread/hardware_destructive_interference_size
- Linux perf. (n.d.). perf-stat(1): Run a command and gather performance counter statistics. Retrieved October 5, 2026, from https://man7.org/linux/man-pages/man1/perf-stat.1.html
Leave a Reply