1

I have a class that has two int fields x and y, and a method Increment that increments both of these fields by dx and dy respectively. I would like to prevent the state of my class to become corrupted by silent arithmetic overflows (which would result to x or y or both becoming negative), so I am explicitly incrementing the fields in a checked block:

class MyClass
{
    private int x;
    private int y;

    public void Increment(int dx, int dy)
    {
        checked { x += dx; y += dy; }
    }
}

This should ensure that in case of arithmetic overflow the caller will receive an OverflowException, and the state of my class will remain intact. But then I realized that an arithmetic overflow could occur in the increment of y, after the x has already successfully incremented, resulting to a different type of state corruption, which is not less disruptive than the first. So I changed the implementation of the Increment method like this:

public void Increment2(int dx, int dy)
{
    int x2, y2;
    checked { x2 = x + dx; y2 = y + dy; }
    x = x2; y = y2;
}

This seems like a logical solution to the problem, but now I am concerned that the compiler could "optimize" my carefully crafted implementation, and reorder the instructions in a way that would allow the x assignment to occur before the y + dy addition, resulting again to state corruption. I would like to ask if, according to the C# specification, this undesirable scenario is possible.

I am also considering removing the checked keyword, and instead compiling my project with the "Check for arithmetic overflow" option enabled (<CheckForOverflowUnderflow>true</CheckForOverflowUnderflow>). Could this make any difference, regarding a possible reordering of the instructions inside the Increment method?


Update: a more succinct arithmetic-overflow-safe implementation is possible by using tuple deconstruction. Is this version any different (less safe) than the verbose implementation?

public void Increment3(int dx, int dy)
{
    (x, y) = checked((x + dx, y + dy));
}

Clarification: The MyClass is intended to be used in a single-threaded application. Thread-safety is not a concern (I know that it's not thread-safe, but it doesn't matter).

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
  • 3
    Coming from the compiler side I would say that if the compiler changes the semantics of the program (e.g. by moving the assignment of x across the overflow check for y) it should be considered as a bug in the compiler. – Johan Sep 05 '21 at 06:29
  • 1
    [Sharplab](https://sharplab.io/#v2:C4LglgNgPgAgTARgLAChXwAQFkCeBhCAQwGdjUBvVDajABwCcwA3Q4AUwzADtgMAPANxUaDZqw7deOIWhQwAzBhgAWDAEkuAY3psAtmx4AKSRgAmfADSceZnAEoKw6ib5wrOODJoZNACzaaANZsphjk/HAYALz8GADUZoIYHtHJ8bYCGAC+TrExrpk4qR4yOShZQA===) says it doesn't reorder. The spec doesn't say anything about reordering except it promises you things about `volatile` reads and writes. – Sweeper Sep 05 '21 at 06:29
  • Thanks @Sweeper for the Sharplab [link](https://sharplab.io/#v2:C4LglgNgPgAgTARgLAChXwAQFkCeBhCAQwGdjUBvVDajABwCcwA3Q4AUwzADtgMAPANxUaDZqw7deOIWhQwAzBhgAWDAEkuAY3psAtmx4AKSRgAmfADSceZnAEoKw6ib5wrOODJoZNACzaaANZsphjk/HAYALz8GADUZoIYHtHJ8bYCGAC+TrExrpk4qR4yOShZQA===). It makes me feel a bit safer. Maybe I am paranoid about this. – Theodor Zoulias Sep 05 '21 at 06:46
  • 1
    Just saw your edit. The deconstruction version compiles to the same IL as the verbose `x = x2; y = y2;` version. Try it on Sharplab :) – Sweeper Sep 05 '21 at 06:49
  • This is a bad, bad idea. All you gained is consistency, it is still corrupt. Since it only makes sense to do this if you catch the exception and continue using the object. Now the code that uses the object starts generating bad results and does so in a completely undiagnosable way. Many programmer hairs were lost due to this kind of state corruption that's so common in, say, C++. Use *checked*, don't defeat its benefit by catching it. – Hans Passant Sep 05 '21 at 06:56
  • @HansPassant so you say that in case my class receives an input that causes an internal arithmetic overflow, the correct behavior is to allow the state of my class to become corrupted, and assume that the user of the class will not handle the `OverflowException`, and will let the process terminate? – Theodor Zoulias Sep 05 '21 at 07:05
  • It just doesn't matter that the class object gets corrupted, you can't use it anymore since it is not capable of representing the state your program wants to be in. A prophylactic way to prevent state corruption is to validate input, reject it when it causes your program to malfunction. You prevent a + b to overflow by verifying that int.MaxValue - a <= b. – Hans Passant Sep 05 '21 at 07:23
  • @HansPassant hmm, is the `int.MaxValue - a <= b` going to work correctly in case the `a` is negative? And how is `if (int.MaxValue - a <= b) throw new OverflowException();` any better than delegating the check to the compiler? I find these manual checks very tricky and mind boggling, and honestly I would prefer to let the compiler do them than myself. – Theodor Zoulias Sep 05 '21 at 07:39
  • 1
    A compliant CLR Jitter *cannot*, by definition, reorder, precisely *because* of the semantics of the overflow checks. Reordering can only happen where there is no observable effect on a single thread, and is only a problem if using multiple threads. C# compiler itself will not reorder, it's not within the specification, and this applies to both versions of your code – Charlieface Sep 05 '21 at 08:00
  • To top it off, there is actually no reason to reorder anyway, because I'd expect two `add`s followed by two `mov`s is going to be faster than `add mov add mov` – Charlieface Sep 05 '21 at 08:08
  • @Charlieface so you say that if the `checked` was missing, the compiler would be allowed to reorder the instructions of the `Increment` method, because there would be no observable effect on a single thread by the reordering. But the presence of the `checked` prohibits the compiler from reordering the instructions, in such a way that would make possible an `OverflowException` to be thrown after a partial mutation of the state of my class. Correct? – Theodor Zoulias Sep 05 '21 at 08:20
  • 1
    The Jitter, not the C# compiler. Yes correct, reordering is not allowed to be observed on the same thread. I'm trying to find a source for this... Got it now, will answer shortly – Charlieface Sep 05 '21 at 08:35
  • @Johan Looks like you're right judging by the spec – Charlieface Sep 05 '21 at 09:02
  • @Sweeper I'd beg to differ, the spec seems pretty clear to me that it's not allowed – Charlieface Sep 05 '21 at 09:02

2 Answers2

2

Make your class immutable. When you want to change something, return a new instance.

class MyClass
{
    private int x;
    private int y;

    public MyClass Increment(int dx, int dy)
    {
        checked
        {
            return new MyClass { x = this.x + dx, y = this.y + dy }; 
        }
    }
}

And in your calling code, you'd replace

myClass.Increment( a, b );

with

myClass = myClass.Increment( a, b );

This ensures your class is always internally consistent.

If you don't want to return a new instance, you can get the same benefit by using an internal readonly struct.

public readonly struct Coords
{
    public int X { get; init; }
    public int Y { get; init; }
}

class MyClass
{
    private Coords _coords;

    public void Increment(int dx, int dy)
    {
        checked
        { 
            var newValue = new Coords { X = _coords.X + dx, Y = _coords.Y + dy };
        }
        _coords = newValue;
    }
}
John Wu
  • 50,556
  • 8
  • 44
  • 80
  • Thanks John for the suggestion. Your idea of encapsulating the mutable state of my class into an immutable `struct` sounds great, but is it any safer than my current approach regarding protection against a possible reordering of instructions? Is it guaranteed that the compiler will not assign the `X` property of the `_coords` field, before doing the `_coords.Y + dy` addition? – Theodor Zoulias Sep 05 '21 at 07:47
  • Yes, it guarantees that.... even if your app in multithreaded. It's not possible to update `_coords` without setting both `Y` and `X`. – John Wu Sep 05 '21 at 23:31
  • According to Charlieface's [answer](https://stackoverflow.com/a/69061927/11178549), it's only guaranteed (by the specification for the CLR and CLI) because of the `checked` context. Without a `checked` context the compiler (or the jitter, I am not sure) would be allowed to do a partial set of `_coords` (`_coords.X = _coords.X + dx`), before even doing the `_coords.Y + dy`, because adding the numbers would be impossible to throw an exception (or have any other observable effect). – Theodor Zoulias Sep 06 '21 at 04:25
  • To be fair, your answer addresses cleverly and effectively the question I've asked in the title, but my real question is hidden inside a body of text and code (and it's about compiler guarantees and such). I did a lousy job at giving a suitable title to the question. – Theodor Zoulias Sep 06 '21 at 04:34
  • I am not sure what you mean by "partial set." Either `newValue` is assigned to `_coords` or it isn't. I suppose a partial initialization of `newValue` might be possible, but that hardly matters, since any exceptions will prevent it from being assigned to `_coords`. – John Wu Sep 06 '21 at 08:10
  • John AFAIK the C# compiler (or the Jitter) is allowed to reorder the instructions of a piece of code. Assigning the `_coords.X` field before the completion of the `newValue` construction, does not violate the specs regarding observable effects (in a non-checked context), so the compiler/jitter probably has the right to do it. This is my understanding at least, but I might be wrong. I am not a compiler expert. – Theodor Zoulias Sep 06 '21 at 08:47
  • Please review again. There is no line of code that assigns a value to `_coords.X`, so there is nothing to reorder. I think you are way overthinking this. – John Wu Sep 06 '21 at 15:52
  • @TheodorZoulias Ultimately, re-assigning a variable is guaranteed to be thread-safe, *as long as* the entire struct is within the CPU register size: 4-byte on 32-bit CPU and 8 on 64-bit. If it's larger then there is simply no guarantee without `Interlocked`. This is also in the ECMA spec. So it doesn't help to "not" re-assign one of the values, because the whole struct is being re-assigned – Charlieface Sep 08 '21 at 20:30
  • @Charlieface this question is not about thread-safety. You can assume that the `MyClass` will be used by a single-threaded application, or -otherwise- that appropriate synchronization will be used to ensure thread-safety. – Theodor Zoulias Sep 08 '21 at 20:55
2

TL;DR; This is perfectly safe to do within a single thread.

The CLI, which implements your code in native machine language, is simply not allowed to reorder instructions in such a fashion as to have a visible side-effect, at least as far as observations from a single thread are concerned. It is forbidden by the specification.

Let's take a look at ECMA-335, the specification for the CLR and CLI, (my bold)

I.12.6.4 Optimization

Conforming implementations of the CLI are free to execute programs using any technology that guarantees, within a single thread of execution, that side-effects and exceptions generated by a thread are visible in the order specified by the CIL.
... snip ...
There are no ordering guarantees relative to exceptions injected into a thread by another thread (such exceptions are sometimes called “asynchronous exceptions” (e.g., System.Threading.ThreadAbortException).

[Rationale: An optimizing compiler is free to reorder side-effects and synchronous exceptions to the extent that this reordering does not change any observable program behavior. end rationale]

[Note: An implementation of the CLI is permitted to use an optimizing compiler, for example, to convert CIL to native machine code provided the compiler maintains (within each single thread of execution) the same order of side-effects and synchronous exceptions.
This is a stronger condition than ISO C++ (which permits reordering between a pair of sequence points) or ISO Scheme (which permits reordering of arguments to functions). end note]

So exceptions must occur in the order specified in the IL code compiled by C#, therefore if an overflow occurs in a checked context, the exception must be thrown before any observation of the next instructions. (In an unchecked context, there is no such guarantee, because there is no exception, however the difference is not observable on a single thread.)

Note that this does not mean that the two additions cannot occur before the stores into local variables or before the two overflow checks, because the locals cannot be observed once an exception is thrown. In a fully optimized build, the locals will probably be stored in CPU registers, and wiped in the event of an exception.

The CPU is also free to reorder internally, so long as the same guarantees apply.


There is one exception to all of this, barring the mentioned multi-threading allowance:

Optimizers are granted additional latitude for relaxed exceptions in methods. A method is E-relaxed for a kind of exception if the innermost custom attribute CompilationRelaxationsAttribute pertaining to exceptions of kind E is present and specifies to relax exceptions of kind E.

However the current Microsoft implementation does not provide such a relaxation option anyway.


As to using the tuple-deconstructing syntax, unfortunately the spec for C# 7 has not been released, but this page on Github indicates that it should also be side-effect free.

Charlieface
  • 52,284
  • 6
  • 19
  • 43