Configuring your environment¶
Sen measures time, and the machine underneath it is not a constant. The same component doing the same work can take visibly different amounts of time from one run to the next, and most of the reasons have nothing to do with your code. This page covers what to do about that, and how Sen reads the clock.
Quieten the machine¶
Start with the easy part. Anything else running competes with Sen for cores, cache and memory bandwidth, and it will show up in your timings. During development, close the browsers, mail clients and chat applications before you take numbers you intend to trust.
Also detach the debugger. An attached debugger changes the timing of the process it is attached to, so a run under the debugger is not a run you can measure.
Why the same work takes different amounts of time¶
Most of the variance you will see comes from how modern processors work.
The core does not execute your instructions in the order you wrote them. It runs independent instructions in parallel, reorders them, and guesses which way branches will go so it can start work before the answer is known. A measurement therefore depends on the code surrounding the part you care about, not only on the part itself. Timing a small piece of code in isolation and timing it in place can give different answers, and both can be correct.
Your core may not be entirely yours. With simultaneous multithreading, two hardware threads share one physical core's execution resources. What the sibling thread is doing changes what is left for you, and it does not change it uniformly: a sibling saturating the floating-point units interferes with your floating-point work far more than with your integer work. So it is not enough to know how loaded the machine is; you need to know with what.
The clock rate is not fixed. Separate mechanisms move it, and each adds noise:
- Turbo. When there is thermal and power headroom, cores run above their rated base frequency. Nothing guarantees the CPU reaches a given turbo frequency, and nothing guarantees it stays there, so a measurement can depend on how long the machine has been working and how warm it is.
- Power saving. Idle cores are dropped to lower frequencies or parked. When work arrives the core has to ramp back up, and that ramp lands in your earliest samples.
- Wide SIMD. On Intel parts, wide vector instructions draw enough extra power that the core lowers its frequency to run them, sometimes sharply. Code that uses them can slow down the code around it.
For repeatable numbers, pin the frequency: disable turbo and disable power saving, and accept the base frequency. You will measure a slower machine, but you will measure the same machine twice.
How Sen reads the clock¶
Reading the time looks free and is not. clock_gettime() and gettimeofday() are system calls;
Linux hands them out through the vDSO so they do not have to trap into the kernel, which makes them
much cheaper than an ordinary system call but still costly enough to matter when Sen is timing work
at its own granularity.
x86 processors offer something cheaper. The TSC (Time Stamp Counter) is a register that a single
instruction, RDTSC, reads from user space in a handful of cycles.
The catch is that the TSC is only a usable clock if it ticks at a steady rate. What Sen needs is an
invariant TSC: one that runs at a constant rate whatever P-, C- or T-state the core is in, and
that is synchronized across cores. Sen asks the processor directly, reading
CPUID.80000007H:EDX[8], and falls back to the ordinary system timers when the bit is not set,
logging a warning as it does so. On Intel parts, that bit means Nehalem or later.
This is x86 only, on both Linux and Windows. On AArch64, including Apple silicon, Sen never takes the TSC path and always uses the system timers.
The processor is not the only thing that can get this wrong: hypervisors emulate the counter, firmware problems and errata affect it, and an operating system can report an invariant TSC that does not behave like one. On Linux you can cross-check what the kernel decided for its own use:
Any other value here, kvm-clock for example, means the kernel did not trust the TSC either.
If you want Sen to leave the TSC alone, set the SEN_AVOID_TSC environment variable. Only its
presence is tested, so any value turns the TSC off. SEN_AVOID_TSC=0 disables it just as firmly as
SEN_AVOID_TSC=1. To switch it back on, remove the variable from the environment instead of giving
it a falsy value.
Thread priority, CPU affinity and stack size¶
Every component runs in its own thread, so these settings apply per component, not to the whole
process. They go on the component's entry in the configuration file, alongside group and
freqHz, and they work the same way under load: and build:.
build:
- name: controlLoop
group: 3
freqHz: 100
priority: nominalMax # lowest | nominalMin | nominalMax | highest
cpuAffinity: 12 # bitmask: bits 2 and 3, so processors 2 and 3
stackSize: 1048576 # bytes; 0 or omitted means the system default
Priority takes one of the names shown above. nominalMin and nominalMax bound what is
intended for ordinary component work; lowest is for background work that should yield to
everything else, and highest is for supervisory work. What these map onto is the operating
system's own scheme, so the practical spread between them depends on the platform and on how the
process was started.
CPU affinity is a bitmask, one bit per processor: 1 is processor 0, 2 is processor 1, 12
is processors 2 and 3. The field is 64 bits wide, so processors beyond the first 64 cannot be
addressed this way.
An affinity Sen cannot apply stops the kernel
If the operating system rejects the mask, which is what happens inside a container whose processor set does not include the processors you asked for, thread creation fails, and a component that cannot create its thread stops the kernel instead of continuing without the setting. The reason is reported, and the process exits with a non-zero code.
The same configuration can behave differently on a developer machine and in a container: a mask that is valid on the host may not be valid inside the container.
Stack size is in bytes and only worth setting when you know a component needs more than the default, or when you are running many components on a memory-constrained target and want less.
CPU shielding¶
Shielding, keeping the operating system's own scheduler away from a set of processors so that only
what you place there runs, is done to the operating system, not to Sen. On Linux that is
isolcpus, cset shield or a cgroup, depending on your distribution and how the machine is
managed.
Sen's part is the second half: once processors are reserved, cpuAffinity is how you put a
component on them. Set the affinity to the shielded processors and give the component a priority
that suits its role, and the two together give it a processor to itself.
Queue sizing¶
Each component also has inQueue and outQueue, which decide how much traffic it will buffer and
what happens when the buffer is full. evictionPolicy takes dropOldest or dropNewest, and
maxSize: 0 means unbounded. These matter when a component cannot keep up with what is being sent
to it, because they decide where the loss appears.