The C# compiler produces different byte codes whether optimisation is turned on or off (off for DEBUG by default, which makes the emitted byte code look more like the source code, for easier pairing with the PDB files). The JIT compiler might take a loop and compile it down to a single inline sequence of instructions if they could make the execution faster. The processor might have multiple pipe lines in which it executes instruction streams in parallel (making use of the processor in times that it would otherwise have been wasting cycles waiting for the RAM or disk to send back data). Even after memory writes have been completed, they may be temporarily buffered close to the CPU rather than being expensively written directly to memory.
All of these changes are examples of
code motion
. To the average C# developer, most of these changes go unnoticed; on a single thread we have virtually no way of ever finding out that the instructions were actually executed out-of-order: the contract of C# guarantees these things for us. But in parallel processing with shared state, we start to witness evidence of the underlying chaos. Fields are updated after we expect - fields are updated before we expect. To limit any negative effects of code motion, the C# designers have given us the keyword, volatile
. The keyword can be applied to instance/static fields (but not method local variables, as these cannot be safely accessed by multiple threads anyway - they will always live on their own thread's stack.) Read/write accesses to memory locations marked as volatile are protected by a "memory barrier". In oversimplified terms, any source code before a volatile read/write is guaranteed to have been completed before the access; similarly any source code after a volatile read/write will only be conducted after the access. To make your code ridiculously safe, you could add volatile to any updatable field (e.g. it can't be const or readonly) but you'd be losing out on all positive benefits of code motion.
No comments:
Post a Comment