FPGA FPGA - 1 month ago 6
C# Question

Does the compiler interpret the following snippets the same way?

Consider

public List<GroupByDateType> GroupByItems
{
get { return Enum.GetValues(typeof(GroupByDateType)).Cast<GroupByDateType>().ToList(); }
}


And

private List<GroupByDateType> _GroupByItems;
public List<GroupByDateType> GroupByItems
{
get {return _GroupByItems??(_GroupByItems=Enum.GetValues(typeof(GroupByDateType)).Cast<GroupByDateType>().ToList());}
}


Usually I go with the second one because when I look at the first one it seems to me that the list is regenerated, but is that really what happens, does the CLR create the backing field for me by any chance?

Answer

No. Though the both the compiler and the jitter are free to perform all manner of optimisations, as a rule the compiler only performs those that are most obvious (in particular, removing dead code), while the jitter tends not to do anything which would add to the layout or have a downside the developer chose to avoid.

Memoisation changes the layout by adding a field, and optimises for speed at the cost of memory and so is something you don't want to happen every time (if you consider the number of repeat calls times the cost of calculating to not be worth the extra memory used). Among other things, deciding on that balance involves factors outside of just this code, such as how many such objects will be created (and hence how much memory used). It wouldn't be a good automatic change across a large number of cases.

There's also the fact that memoisation is easy; if you'd wanted it, you'd have done it, if you didn't want it you wouldn't have (and indeed, certainly shouldn't every time, only when it has a real advantage).

Conversely, memorisation that covers null as a possible result is slightly trickier, and that which must be threadsafe trickier again (there's a few different approaches that work in different concurrent cases, all of which are easy in themselves, but picking which to use often isn't). It would be totally inappropriate for an "optimisation" to turn a method (including a property getter) that was threadsafe into one that was not, or at least changed the behaviour of the object in the face of concurrent calls.

Memoisation can also sometimes have subtle semantic differences such as your example; the behaviour differs if calling code changes the list. This could be vital (the list is examined for such changes by other code) or a bug (the next caller gets a changed list that is incorrect) or irrelevant (it's internal-only, and you don't change it anywhere so you're cutting out the cost of safeguarding). Not all memoisation has this issue (one could return a read only list, for example), but it does mean it couldn't be done automatically.

Comments