Showing posts with label dev. Show all posts
Showing posts with label dev. Show all posts

Saturday, 6 January 2024

Modern C# & Performance (in brief)

This video came up in my youtube feed the other day (it's only 12 mins long, go check it out): 


In it the presenter shows some code written in classic, old-school imperative C#, and then progressively factors it into his ideal of modern C#.  

The algorithm he is implementing takes a sequence of strings, looks at all the ones which can be parsed as an int, and returns the total of the two largest values.  It returns -1 as an error code (so we can assume that the numbers passed in are all >= 0).

So, he starts with this:

 
 int GetMagicSum0(IEnumerable<string> items) {
     List<int> all = new List<int>();
     using (IEnumerator<string> enumerator = items.GetEnumerator()) {
         while (enumerator.MoveNext()) {
             int number;
             if (int.TryParse(enumerator.Current, out number))
               all.Add(number);
         }
     }

     if (all.Count < 2)
       return -1;

     int[] greatestTwo = {all[0], all[1]};
     if (greatestTwo[1] > greatestTwo[0]) {
         greatestTwo[0] = all[1];
         greatestTwo[1] = all[0];
     }

     for (int i = 2; i < all.Count; i++) {
         if (all[i] > greatestTwo[0]) {
             greatestTwo[1] = greatestTwo[0];
             greatestTwo[0] = all[i];
         }
         else if (all[i] > greatestTwo[1]) {
             greatestTwo[1] = all[i];
         }
     }

     return greatestTwo[0] + greatestTwo[1];
 }


And over a few iterations ends up with this:


 int GetMagicSum3(IEnumerable<string> items) =>
     items.Aggregate((max: 0, next: 0, count: 0), AdvanceRaw, ProduceSum);

 int ProduceSum((int max, int next, int count) tuple) =>
     tuple.count == 2 ? tuple.max + tuple.next : -1;

 (int max, int next, int count) AdvanceRaw((int max, int next, int count) tuple, string item) =>
     int.TryParse(item, out int number) ? Advance(tuple, number) : tuple;

 (int max, int next, int count) Advance((int max, int next, int count) tuple, int number) => tuple switch
 {
     (_, _, 0) => (number, 0, 1),
     (var max, _, 1) when number > max => (number, max, 2),
     (var max, _, 1) => (max, number, 2),
     (var max, _, 2) when number > max => (number, max, 2),
     (var max, var next, 2) when number > next => (max, number, 2),
     _ => tuple,
 };


...my blog page is set up pretty narrow, so here's an image of the code that you can zoom (I have nothing against long lines, I wanted to present it as text above so it was copy-pastable):


Now, we can argue about which code is better or more readable, but my first thought was that the performance is going to suffer.  I was then galvanized by this comment on the video:


This seemed super unlikely to me, so I decided to measure it, yielding these initial results:


Which bear out his response: the functional GetMagicSum3 does indeed run faster than the imperative GetMagicSum0... but you knew there was a "but" coming, right?  Did you notice the problem with GetMagicSum0?  Go back and look at it and see if you can spot it.

This is the setup for the test program btw: 


 string CSharpPerformanceTest(int wordCount = 100000000) {
     if (wordCount < 2) return "wordCount must be >= 2";

     System.Random rng = new((int)System.DateTime.Now.Ticks);
     int randomInt() => rng.Next() / 2; // halved so don't overflow

     List<string> numbers = new ();
     for (int i = 0; i < wordCount; i++)
         numbers.Add(randomInt().ToString());

     // Then time a call to each GetMagicSumX(numbers);


Did you see what was wrong with GetMagicSum0?  The problem with is is it generates a List<string> of the IEnumerable, completely unnecessarily!  Horvat does explicitly say that the algorithm isn't the point of the video - the style differences are - but I don't think pointing out that it's making a completely unneeded copy of the data really counts as algorithmic nit-picking!

If we're comparing imperative vs functional then I think it's fair that we use the best possible form of each.  We can assume the functional GetMagicSum3 is as good as can be, as the point of the video is to advocate for it, so let's make a good version of GetMagicSum0, we'll call it GetMagicSumA.  

Mostly we're simply going to remove the List generated at the start.  However, because of how IEnumerable works, setting up the greatestTwo array and then iterating from index 2 would be a pain: you'd need to make an IEnumerator, check Current was there and parsed, MoveNext(), check Current again, until you got two numbers...  sounds kind of loop-y.  Happily we already have a loop that does all that logic, so we'll simply use it to do all the work.  We set up greatestTwo so that it will always be filled out by the first two numbers, by setting it to { -1, -1 }. (If we wanted to handle negative numbers we'd use int.MinValue instead, but as we're only dealing with positive numbers, -1 is more readable).  This gets us:


 int _GetMagicSumA(IEnumerable<string> items) {
     int[] greatestTwo = { -1, -1 };

     foreach (string item in items) {
         if (int.TryParse(item, out int number)) {
             if (number > greatestTwo[0]) {
                 greatestTwo[1] = greatestTwo[0];
                 greatestTwo[0] = number;
             }
             else if (number > greatestTwo[1]) {
                 greatestTwo[1] = number;
             }
         }
     }

     if (greatestTwo[1] == -1) // didn't find two numbers
         return -1;

     return greatestTwo[0] + greatestTwo[1];
 }


Now, clearly I'm partisan; Horvat is bringing his best functional code to the fight, so in balance I'm going to represent imperative as best as I can.  To me, that greatestTwo array isn't the clearest thing to reason about in the inner if block, and I'd prefer not to nest quite so deep, so I'm going to refine this to the following semantically identical function: 


 int GetMagicSumA(IEnumerable<string> items) {
     int ultimate = -1;
     int penultimate = -1;

     foreach (string item in items) {
         if (!int.TryParse(item, out int number))
             continue;

         if (number > ultimate) {
             penultimate = ultimate;
             ultimate = number;
         }
         else if (number > penultimate) {
             penultimate = number;
         }
     }

     if (penultimate == -1) // didn't find two numbers
         return -1;

     return ultimate + penultimate;
 }


Comparing the readability of the imperative vs functional code is subjective, you can decide which version you find easier to reason about for yourself: below is GetMagicSum3 again for comparison.  It takes up 18 lines on the screen, while the above GetMagicSumA takes up 22 (I do tend to space my code out a fair bit).


However, the point of this post is not to argue about style, but about performance, and here are the results:


With the unnecessary List generation removed, the imperative code is indeed faster: GetMagicSum3 takes 1.26x as long as GetMagicSumA.

Follow-up for game developers


This section is for people who really care about performance, so if that's not you please don't get your knickers in a twist.
You probably noticed that in addition to GetMagicSumA, there are two successively faster versions of the function profiled above.  If you're using C# for writing games then this bit is for you: DO NOT USE IENUMERABLE.  I have never constructed an IEnumerable in any game, ever.  For sequences of homogenous data you need two types: List<T> and Array<T>.  If your data is sitting in memory there is no reason to use an IEnumerable.  If it's being generated an item at a time, then you need to be dealing with that asynchronicity with code that reflects it.
There is a very handy interface called IList, which will accept both List and Array, so is the perfect replacement here.  In essence this is the only change between GetMagicSumB and GetMagicSumA:


 int GetMagicSumB(IList<string> items) {
     int ultimate = -1;
     int penultimate = -1;

     for (int i = 0; i < items.Count; i++) {
         if (!int.TryParse(items[i], out int number))
             continue;

         if (number > ultimate) {
             penultimate = ultimate;
             ultimate = number;
         }
         else if (number > penultimate) {
             penultimate = number;
         }
     }

     if (penultimate == -1) // didn't find two numbers
         return -1;

     return ultimate + penultimate;
 }


As you can see the two difference in the code are the parameter type, and using a for instead of a foreach; the result is substantially faster code.  Using GetMagicSumB as the baseline, GetMagicSumA takes 1.19x as long as it, and GetMagicSum3 takes 1.5x as long.

Bonus Round


The table of timings contains a final form of the function, GetMagicSumC, which is marginally faster again.  It's clear in the original video that the point is not about optimising the algorithm, so this isn't relevant to the post in general.  Therefor, an exercise for the reader: how do you modify GetMagicSumB to get GetMagicSumC, and that small gain in performance? Clue: .

Wednesday, 7 August 2013

Pigment & The Droid, Part 3

As foreshadowed at the end of Part 2, the optimisations described therein were not enough to get Pigment to run on the Motorola Droid. Pigment was still using too much memory for the Droid to handle.

So: to get Pigment to run on a Droid it needed to use less heap.  How to accomplish this?  It needed to hold less graphical data.  I'd already reduced image memory size as much as possible regarding format and storage options (such as removing the alpha channel);  I either had to load less of the assets, or generate less content as the game ran.  In the end I did both!

The first saving relies on how the user experiences the game; as they play and complete levels the game will send messages to them: the Win! message, the tip about pressing Back to undo, the Bonus Unlocked messages, the Congratulations on completing worlds / the game.  Of these: the Science  Unlocked message and the final Congratulations message on completing the game made a useful pair.

Sunday, 18 November 2012

The Amazon FAotD Experience

If you look on the web for posts by developers regarding Amazon's Free App of the Day feature, you'll find a lot of criticism for it.  Amazon didn't do themselves any favours when they started their App Store; an early-doors about-face on their FAotD policy cost them a lot of developer goodwill.  When originally announced, developers were told they would receive 20% of their app's list price on every download: a very appealing prospect.

However, before they rolled out the facility, Amazon changed this: developers would have to opt-in to the FAotD, under the condition that there would be no payments made at all.  The user would download the app for free, and Amazon would not be paying the developers anything for that day.
That coloured opinion fairly strongly, and other faults with Amazon's App Store were viewed harshly.  In it's original form, user reviews were completely sacrosanct from the developer: they could not contact the user who wrote it, or give any feedback that other users could see.

Amazon also take a very strong view on maintaining the lowest available price for every app it stocks; a developer is not supposed to offer their app on a different store for a lower price.  This means that the developer will be admonished by Amazon if they were to, for example, have a sale day on Google Play where they sold their app for half price.  Note how this contrasts with the FAotD: Amazon say it's OK for the version on their store to be sold cheaper, but it's definitely not OK for the reverse to happen.


This is the viewpoint I read about when researching the Amazon App Store.  Why was I looking at the Amazon App Store?  Unlike on the iPhone, where Apple's App Store is the only option available, on Android there are multiple App Stores, and it's recommended that you get your app out on as many as you can to maximise user exposure.  There generally is no cost to doing so (Amazon have a yearly fee, but it is currently being waived), but the downside is that every store your app is available from is another store you have to update with patches, create graphical assets for, maintain descriptions and version histories... basically, the more time and effort you have to invest.

Wednesday, 7 November 2012

Pigment & the Droid, Part 2

As outlined in Part 1, the HTC Desire bracketed the heap allocation of Pigment.  That is, while developing the game, if it failed to work due to lack of memory then it would be cut back in order to fit, and thus the game came to be defined, at least in terms of heap allocation, by the Desire.  However, there came a point in development where more assets were required than could fit - there seemed to be no more screens to cut, no bitmaps that could be not-loaded, but something had to give in order for the game to work.


In order to fit more assets into memory than could physically go, Pigment uses a trick based on how some of the assets relate to each other.  One group of bitmaps the game uses are the messages, the info screens sent to the user after certain events; the "Win!" message, the tip about pressing Back to undo, the Bonus Unlocked messages, the congratulations message for completing worlds / the game.  Of these, the Science Unlocked message and the final congratulations message on completing the game made a useful pair.

The Science Unlocked message always (and only) displays after you complete your 4th level.  It doesn't matter what level it is, but when you have completed four levels total the Science Unlocked message is displayed (and, of course, the Science skin is unlocked in the Bonus menu).

The End Game message, on the other hand, only displays once you have completed every single level in the game.  Clearly it is very unlikely that a user will see both these messages in one play session.

Monday, 5 November 2012

Pigment & the Droid, Part 1

When Pigment became available as a free download on Amazon's FAotD feature it became clear very quickly that there was a problem: it didn't work on some Motorola handsets - the Droid 4, Droid Razr and Razr Maxx were all reported specifically by unimpressed customers.  So: why did it crash on those phones, and how could it be fixed?


While the Amazon App Store beats Google Play hands-down at promoting your app, it is lacking in Play's technical features. Notably, if your app crashes on a Play user's phone, then they have the option to send you a report which tells you pretty specifically what went wrong.  Amazon do not (yet) have this facility.  As such, there was no way to know what was going wrong with Pigment, aside from guessing.

Educated guessing.  For one thing, customers were reporting it was crashing on the splash screen: the splash screen is displayed while the game loads its assets into memory.  For another, these type of crashes happened often during development: they occurred when the device ran out of memory.

"Out of memory?", you say, "But my phone has 16GB of internal storage!"  The memory we're talking about is the phone's temporary storage, not the place it permanently saves things.  That 16GB is the phone's equivalent of hard disk space - what we're running out of is RAM.
"But my Razr has 1GB of RAM!  How can you be using all of that up?"  Ah, a good question, the answer to which is found in how Android works: