burn-console Part III: The Most Efficient MSH Script
Now that we have our script profiler up and running, we instrument our script a little to mark regions we are concerned about. You can download that starting script here: [burn-console-1.profiler.msh] After running the script profiler, we get the performance breakdown:
Breakdown by line:
----------------------------
15%: Line 123 - if($colour -lt 20) { $colour -= 1 }
14%: Line 122 - if($colour -le 70) { $colour -= 3 }
11%: Line 124 - if($colour -lt 0) { $colour = 0 }
10%: Line 128 - $tempWorkingBuffer[$baseOffset -
8%: Line 117 - $colour /= 4.0
7%: Line 121 - if($colour -gt 70) { $colour -= 1 }
6%: Line 154 - $nextScreen[$row, $column] = `
6%: Line 113 - $colour = $screenBuffer[$baseOffset]
6%: Line 115 - $colour += $screenBuffer[$baseOffset + 1]
6%: Line 116 - $colour += $screenBuffer[$baseOffset +
5%: Line 114 - $colour += $screenBuffer[$baseOffset - 1]
5%: Line 109 - $baseOffset = ($windowWidth * $row) +
0%: Line 90 - if($random.NextDouble() -ge 0.20)
0%: Line 152 - for($column = 0; $column -lt $windowWidth;
0%: Line 94 - $screenBuffer[($windowHeight - 2) *
Breakdown by marked regions:
----------------------------
6%: updateScreen
0%: startFireLastRow
93%: propigate
0%: Unmarked
0%: main
It looks like we’re spending 93% of our time propagating the fire. When we investigate how we’re spending the time, none of the hot spots are individually egregious. Since we’re in such a tight loop, we’re spending most of the time comparing colours. We can be a little smarter with our ‘if’ statements, attempting to minimize the number of comparisons and variable assignments. Before optimization, we have 4 checks per loop, and (a weighted average of) just over 1 assignment per loop. If we write it as below, we reduce that to (a weighted average of) 1 check per loop, and (a weighted average even closer to) 1 assignment per loop.
if($colour -gt 70)
{
$colour -= 1
}
else
{
$colour -= 3
if($colour -lt 1)
{
$colour = 0
}
elseif($colour -lt 20)
{
$colour -= 1
}
}
This brings us up to 0.57 frames per second. A great improvement, but we’re obviously not done yet. We run the profiler again, and see that the hotspots have moved:
Breakdown by line:
----------------------------
13%: Line 141 - $tempWorkingBuffer[$baseOffset -
9%: Line 113 - $colour = $screenBuffer[$baseOffset]
9%: Line 117 - $colour /= 4.0
9%: Line 115 - $colour += $screenBuffer[$baseOffset + 1]
9%: Line 131 - $colour = 0
8%: Line 129 - if($colour -lt 1)
8%: Line 127 - $colour -= 3
7%: Line 121 - if($colour -gt 70)
7%: Line 116 - $colour += $screenBuffer[$baseOffset +
7%: Line 114 - $colour += $screenBuffer[$baseOffset - 1]
5%: Line 109 - $baseOffset = ($windowWidth * $row) +
5%: Line 167 - $nextScreen[$row, $column] = `
3%: Line 244 - $bufferCell = `
1%: Line 240 - $character =
0%: Line 252 - $paletteIndex++
0%: Line 241 - $fgColour =
0%: Line 242 - $bgColour =
0%: Line 165 - for($column = 0; $column -lt $windowWidth;
Breakdown by marked region:
----------------------------
5%: updateScreen
0%: startFireLastRow
90%: propigate
0%: Unmarked
5%: main
There’s not too much we can do, but there are a few lines representing access and manipulation of the $colour variable as we compute the average. So we’ll put those into one line:
$colour = ($screenBuffer[$baseOffset] +
$screenBuffer[$baseOffset - 1] +
$screenBuffer[$baseOffset + 1] +
$screenBuffer[$baseOffset + $windowWidth]) / 4.0
The frame rate barely changes, so let’s see what the new hot spots are:
Breakdown by line:
----------------------------
19%: Line 140 - $tempWorkingBuffer[$baseOffset -
13%: Line 113 - $colour = ($screenBuffer[$baseOffset] +
12%: Line 109 - $baseOffset = ($windowWidth * $row) +
11%: Line 126 - $colour -= 3
11%: Line 128 - if($colour -lt 1)
11%: Line 120 - if($colour -gt 70)
10%: Line 130 - $colour = 0
9%: Line 166 - $nextScreen[$row, $column] = `
1%: Line 240 - $fgColour =
1%: Line 243 - $bufferCell = `
0%: Line 239 - $character =
0%: Line 122 - $colour -= 1
0%: Line 251 - $paletteIndex++
0%: Line 106 - for($column = 1; $column -lt ($windowWidth - 1);
0%: Line 219 - if($apparentBrightness -gt
Breakdown by marked region:
----------------------------
9%: updateScreen
0%: startFireLastRow
87%: propigate
0%: Unmarked
4%: main
There’s really not much left for us to optimize. We seem to be spending a lot of time computing the $baseOffset variable, though. Although we can’t improve the efficiency of the statement, we can execute it less frequently. Rather than compute it from scratch each time via multiplication, we’ll simply increment it once per column.
We can continue with this iterative refinement, giving an end result of about 0.68 frames per second – 70% faster than when we started. You can download the optimized version here: [burn-console-2.working.msh]. The problems have now shifted to things we have no control over:
Breakdown by line:
----------------------------
20%: Line 171 - $nextScreen[$row, $column] = `
20%: Line 111 - $colour = ($screenBuffer[$baseOffset] +
18%: Line 141 - $tempWorkingBuffer[$baseOffset -
17%: Line 144 - $baseOffset++
17%: Line 118 - if($colour -gt 0)
1%: Line 120 - if($colour -gt 70)
1%: Line 248 - $bufferCell = `
1%: Line 126 - $colour -= 3
1%: Line 122 - $colour -= 1
0%: Line 134 - $colour -= 1
0%: Line 128 - if($colour -lt 1)
0%: Line 246 - $bgColour =
0%: Line 94 - $screenBuffer[($windowHeight - 2) *
0%: Line 130 - $colour = 0
0%: Line 162 - $nextScreen =
Breakdown by marked region:
----------------------------
20%: updateScreen
1%: startFireLastRow
77%: propigate
0%: Unmarked
3%: main
To solve this problem, we’ll delve into a trick from the Demo Scene. Did you ever use inline assembler in C? We’ll do something similar, but with inline C# in MSH.
[Edit: Monad has now been renamed to Windows PowerShell. This script or discussion may require slight adjustments before it applies directly to newer builds.]