Introduction
This tip will explain how to calculate progress values correctly. It's not about C# tasks or async programming. Though the code is written in C#, this tip applies to any programming language.
Background
Let's assume we have a large amount of data to process and want to give the user some kind of feedback on the progress, like a progressbar in a user interface. If we have a couple of million data points to process, it doesn't make sense to update the progressbar in each step. For N
data points, we want to report progress no more than PROGRESS_MAX
times.
Method 1
You might find yourself writing code like this:
const int PROGRESS_MAX = 100;
int percent = 0;
int total = Math.Max(1, N - 1);
for (int i = 0; i < N; i++)
{
ProcessDataPoint(data[i]);
if ((PROGRESS_MAX * i) / total != percent)
{
percent = (PROGRESS_MAX * i) / total;
ProgressChanged(percent);
}
}
This will work fine for small N
and small PROGRESS_MAX
, but for larger values, the multiplication in...
if ((PROGRESS_MAX * i) / total != percent)
...might overflow. Using uint
or long
can solve the problem, but there's a better solution, which is more efficient.
Method 2
The following code works fine if N
is larger than PROGRESS_MAX
:
const int PROGRESS_MAX = 100;
double step = N / (double)PROGRESS_MAX;
double current = 0.0;
for (int i = 0; i < N; i++)
{
ProcessDataPoint(data[i]);
if (i >= current)
{
current += step;
ProgressChanged((int)((current / step) + 0.5));
}
}
If N
might be smaller than PROGRESS_MAX
, the following code can be used:
const int PROGRESS_MAX = 100;
double step = N / (double)PROGRESS_MAX;
double current = 0.0;
for (int i = 0; i < N; i++)
{
ProcessDataPoint(data[i]);
if (i >= current)
{
current += step;
if (step < 1.0)
{
ProgressChanged((int)(((i + 1) / step) + 0.5));
}
else
{
ProgressChanged((int)((current / step) + 0.5));
}
}
}
Benchmark
Though processing the data will usually dominate the runtime, here are the benchmark results of both methods:
Size | Calls Method 1 | Time | Calls Method 2 | Time |
50 | 49 | 0ms | 49 | 0ms |
100 | 99 | 0ms | 99 | 0ms |
101 | 100 | 0ms | 100 | 0ms |
2100 | 100 | 0ms | 100 | 0ms |
5000000 | 100 | 87ms | 100 | 6ms |
11119999 | 100 | 198ms | 100 | 14ms |
211119999 | 104 | 3839ms | 100 | 273ms |
In the last row, you can see that method 1 would raise the progress changed event 104 times, though PROGRESS_MAX
is set to 100. This is due to arithmetic overflow.
Benchmark code:
const int PROGRESS_MAX = 100;
private static void Test()
{
int[] values = { 50, 100, 101, 2100, 5000000, 11119999, 211119999 };
Console.WriteLine(" Size Calls 1 Time 1 Calls 2 Time 2");
foreach (var item in values)
{
Benchmark(item);
}
}
private static void Benchmark(int N)
{
var timer = new System.Diagnostics.Stopwatch();
Console.Write("{0,12}", N);
int percent = 0, j = 0;
int total = (N - 1);
timer.Start();
for (int i = 0; i < N; i++)
{
if ((PROGRESS_MAX * i) / total != percent)
{
j++;
percent = (PROGRESS_MAX * i) / total;
}
}
timer.Stop();
Console.Write("{0,12}", j);
Console.Write("{0,10}ms", timer.ElapsedMilliseconds);
j = 0;
double step = N / (double)PROGRESS_MAX;
double current = 0.0;
timer.Restart();
for (int i = 0; i < N; i++)
{
if (i > current)
{
j++;
current += step;
}
}
timer.Stop();
Console.Write("{0,12}", j);
Console.Write("{0,10}ms", timer.ElapsedMilliseconds);
Console.WriteLine();
}
Studied math and computer science at the university of Dortmund.
MCTS .NET Framework 4, Windows Applications