28

I was just benchmarking multiple algorithms to find the fastest way to load all data in my app when I discovered that the WP7 version of my app running on my Lumia 920 loads the data 2 times as fast as the WP8 version running on the same device.

I than wrote the following independent code to test performance of the StorageFile from WP8 and the IsolatedStorageFile from WP7.

To clarify the title, here my preliminary benchmark results I did, reading 50 files of 20kb and 100kb:

enter image description here

For the code, see below

Update

After doing benchmarks for a few hours today and some interesting results, let me rephrase my questions:

  1. Why is await StreamReader.ReadToEndAsync() consistently slower in every benchmark than the non async method StreamReader.ReadToEnd()? (This might already be answered in a comment from Neil Turner)

  2. There seems to be a big overhead when opening a file with StorageFile, but only when it is opened in the UI thread. (See difference in loading times between method 1 and 3 or between 5 and 6, where 3 and 6 are about 10 times faster than the equivalent UI thread method)

  3. Are there any other ways to read the files that might be faster?

Update 3

Well, now with this Update I added 10 more algorithms, reran every algorithm with every previously used file size and number of files used. This time each algorithm was run 10 times. So the raw data in the excel file is an average of these runs. As there are now 18 algorithms, each tested with 4 file sizes (1kb, 20kb, 100kb, 1mb) for 50, 100, and 200 files each (18*4*3 = 216), there were a total of 2160 benchmark runs, taking a total time of 95 minutes (raw running time).

Update 5

Added benchmarks 25, 26, 27 and ReadStorageFile method. Had to remove some text because the post had over 30000 characters which is apparently the maximum. Updated the Excel file with new data, new structure, comparisons and new graphs.

The code:

public async Task b1LoadDataStorageFileAsync()
{
    StorageFolder data = await ApplicationData.Current.LocalFolder.GetFolderAsync("benchmarks");
    data = await data.GetFolderAsync("samplefiles");
    //b1 
    for (int i = 0; i < filepaths.Count; i++)
    {
        StorageFile f = await data.GetFileAsync(filepaths[i]);
        using (var stream = await f.OpenStreamForReadAsync())
        {
            using (StreamReader r = new StreamReader(stream))
            {
                filecontent = await r.ReadToEndAsync();
            }
        }
    }
}
public async Task b2LoadDataIsolatedStorage()
{
    using (var store = IsolatedStorageFile.GetUserStoreForApplication())
    {
        for (int i = 0; i < filepaths.Count; i++)
        {
            using (var stream = new IsolatedStorageFileStream("/benchmarks/samplefiles/" + filepaths[i], FileMode.Open, store))
            {
                using (StreamReader r = new StreamReader(stream))
                {
                    filecontent = r.ReadToEnd();
                }
            }
        }
    }
    await TaskEx.Delay(0);
}

public async Task b3LoadDataStorageFileAsyncThread()
{
    StorageFolder data = await ApplicationData.Current.LocalFolder.GetFolderAsync("benchmarks");
    data = await data.GetFolderAsync("samplefiles");

    await await Task.Factory.StartNew(async () =>
    {
        for (int i = 0; i < filepaths.Count; i++)
        {

            StorageFile f = await data.GetFileAsync(filepaths[i]);
            using (var stream = await f.OpenStreamForReadAsync())
            {
                using (StreamReader r = new StreamReader(stream))
                {
                    filecontent = await r.ReadToEndAsync();
                }
            }
        }
    });
}
public async Task b4LoadDataStorageFileThread()
{
    StorageFolder data = await ApplicationData.Current.LocalFolder.GetFolderAsync("benchmarks");
    data = await data.GetFolderAsync("samplefiles");

    await await Task.Factory.StartNew(async () =>
    {
        for (int i = 0; i < filepaths.Count; i++)
        {

            StorageFile f = await data.GetFileAsync(filepaths[i]);
            using (var stream = await f.OpenStreamForReadAsync())
            {
                using (StreamReader r = new StreamReader(stream))
                {
                    filecontent = r.ReadToEnd();
                }
            }
        }
    });
}
public async Task b5LoadDataStorageFile()
{
    StorageFolder data = await ApplicationData.Current.LocalFolder.GetFolderAsync("benchmarks");
    data = await data.GetFolderAsync("samplefiles");
    //b5
    for (int i = 0; i < filepaths.Count; i++)
    {
        StorageFile f = await data.GetFileAsync(filepaths[i]);
        using (var stream = await f.OpenStreamForReadAsync())
        {
            using (StreamReader r = new StreamReader(stream))
            {
                filecontent = r.ReadToEnd();
            }
        }
    }
}
public async Task b6LoadDataIsolatedStorageThread()
{
    using (var store = IsolatedStorageFile.GetUserStoreForApplication())
    {
        await Task.Factory.StartNew(() =>
            {
                for (int i = 0; i < filepaths.Count; i++)
                {
                    using (var stream = new IsolatedStorageFileStream("/benchmarks/samplefiles/" + filepaths[i], FileMode.Open, store))
                    {
                        using (StreamReader r = new StreamReader(stream))
                        {
                            filecontent = r.ReadToEnd();
                        }
                    }
                }
            });
    }
}
public async Task b7LoadDataIsolatedStorageAsync()
{
    using (var store = IsolatedStorageFile.GetUserStoreForApplication())
    {
        for (int i = 0; i < filepaths.Count; i++)
        {
            using (var stream = new IsolatedStorageFileStream("/benchmarks/samplefiles/" + filepaths[i], FileMode.Open, store))
            {
                using (StreamReader r = new StreamReader(stream))
                {
                    filecontent = await r.ReadToEndAsync();
                }
            }
        }
    }
}
public async Task b8LoadDataIsolatedStorageAsyncThread()
{
    using (var store = IsolatedStorageFile.GetUserStoreForApplication())
    {
        await await Task.Factory.StartNew(async () =>
        {
            for (int i = 0; i < filepaths.Count; i++)
            {
                using (var stream = new IsolatedStorageFileStream("/benchmarks/samplefiles/" + filepaths[i], FileMode.Open, store))
                {
                    using (StreamReader r = new StreamReader(stream))
                    {
                        filecontent = await r.ReadToEndAsync();
                    }
                }
            }
        });
    }
}


public async Task b9LoadDataStorageFileAsyncMy9()
{
    StorageFolder data = await ApplicationData.Current.LocalFolder.GetFolderAsync("benchmarks");
    data = await data.GetFolderAsync("samplefiles");

    for (int i = 0; i < filepaths.Count; i++)
    {
        StorageFile f = await data.GetFileAsync(filepaths[i]);
        using (var stream = await f.OpenStreamForReadAsync())
        {
            using (StreamReader r = new StreamReader(stream))
            {
                filecontent = await Task.Factory.StartNew<String>(() => { return r.ReadToEnd(); });
            }
        }
    }
}

public async Task b10LoadDataIsolatedStorageAsyncMy10()
{
    using (var store = IsolatedStorageFile.GetUserStoreForApplication())
    {
        //b10
        for (int i = 0; i < filepaths.Count; i++)
        {
            using (var stream = new IsolatedStorageFileStream("/benchmarks/samplefiles/" + filepaths[i], FileMode.Open, store))
            {
                using (StreamReader r = new StreamReader(stream))
                {
                    filecontent = await Task.Factory.StartNew<String>(() => { return r.ReadToEnd(); });
                }
            }
        }
    }
}
public async Task b11LoadDataStorageFileAsyncMy11()
{
    StorageFolder data = await ApplicationData.Current.LocalFolder.GetFolderAsync("benchmarks");
    data = await data.GetFolderAsync("samplefiles");

    for (int i = 0; i < filepaths.Count; i++)
    {
        await await Task.Factory.StartNew(async () =>
            {
                StorageFile f = await data.GetFileAsync(filepaths[i]);
                using (var stream = await f.OpenStreamForReadAsync())
                {
                    using (StreamReader r = new StreamReader(stream))
                    {
                        filecontent = r.ReadToEnd();
                    }
                }
            });
    }
}

public async Task b12LoadDataIsolatedStorageMy12()
{
    using (var store = IsolatedStorageFile.GetUserStoreForApplication())
    {
        for (int i = 0; i < filepaths.Count; i++)
        {
            await Task.Factory.StartNew(() =>
                {
                    using (var stream = new IsolatedStorageFileStream("/benchmarks/samplefiles/" + filepaths[i], FileMode.Open, store))
                    {
                        using (StreamReader r = new StreamReader(stream))
                        {
                            filecontent = r.ReadToEnd();
                        }
                    }
                });
        }
    }
}

public async Task b13LoadDataStorageFileParallel13()
{
    StorageFolder data = await ApplicationData.Current.LocalFolder.GetFolderAsync("benchmarks");
    data = await data.GetFolderAsync("samplefiles");
    List<Task> tasks = new List<Task>();
    for (int i = 0; i < filepaths.Count; i++)
    {
        int index = i;
        var task = await Task.Factory.StartNew(async () =>
        {
            StorageFile f = await data.GetFileAsync(filepaths[index]);
            using (var stream = await f.OpenStreamForReadAsync())
            {
                using (StreamReader r = new StreamReader(stream))
                {
                    String content = r.ReadToEnd();
                    if (content.Length == 0)
                    {
                        //just some code to ensure this is not removed by optimization from the compiler
                        //because "content" is not used otherwise
                        //should never be called
                        ShowNotificationText(content);
                    }
                }
            }
        });
        tasks.Add(task);
    }
    await TaskEx.WhenAll(tasks);
}

public async Task b14LoadDataIsolatedStorageParallel14()
{
    List<Task> tasks = new List<Task>();
    using (var store = IsolatedStorageFile.GetUserStoreForApplication())
    {
        for (int i = 0; i < filepaths.Count; i++)
        {
            int index = i;
            var t = Task.Factory.StartNew(() =>
            {
                using (var stream = new IsolatedStorageFileStream("/benchmarks/samplefiles/" + filepaths[index], FileMode.Open, store))
                {
                    using (StreamReader r = new StreamReader(stream))
                    {
                        String content = r.ReadToEnd();
                        if (content.Length == 0)
                        {
                            //just some code to ensure this is not removed by optimization from the compiler
                            //because "content" is not used otherwise
                            //should never be called
                            ShowNotificationText(content);
                        }
                    }
                }
            });
            tasks.Add(t);
        }
        await TaskEx.WhenAll(tasks);
    }
}

public async Task b15LoadDataStorageFileParallelThread15()
{
    StorageFolder data = await ApplicationData.Current.LocalFolder.GetFolderAsync("benchmarks");
    data = await data.GetFolderAsync("samplefiles");

    await await Task.Factory.StartNew(async () =>
        {
            List<Task> tasks = new List<Task>();
            for (int i = 0; i < filepaths.Count; i++)
            {
                int index = i;
                var task = await Task.Factory.StartNew(async () =>
                {
                    StorageFile f = await data.GetFileAsync(filepaths[index]);
                    using (var stream = await f.OpenStreamForReadAsync())
                    {
                        using (StreamReader r = new StreamReader(stream))
                        {
                            String content = r.ReadToEnd();
                            if (content.Length == 0)
                            {
                                //just some code to ensure this is not removed by optimization from the compiler
                                //because "content" is not used otherwise
                                //should never be called
                                ShowNotificationText(content);
                            }
                        }
                    }
                });
                tasks.Add(task);
            }
            await TaskEx.WhenAll(tasks);
        });
}

public async Task b16LoadDataIsolatedStorageParallelThread16()
{
    await await Task.Factory.StartNew(async () =>
        {
            List<Task> tasks = new List<Task>();
            using (var store = IsolatedStorageFile.GetUserStoreForApplication())
            {
                for (int i = 0; i < filepaths.Count; i++)
                {
                    int index = i;
                    var t = Task.Factory.StartNew(() =>
                    {
                        using (var stream = new IsolatedStorageFileStream("/benchmarks/samplefiles/" + filepaths[index], FileMode.Open, store))
                        {
                            using (StreamReader r = new StreamReader(stream))
                            {
                                String content = r.ReadToEnd();
                                if (content.Length == 0)
                                {
                                    //just some code to ensure this is not removed by optimization from the compiler
                                    //because "content" is not used otherwise
                                    //should never be called
                                    ShowNotificationText(content);
                                }
                            }
                        }
                    });
                    tasks.Add(t);
                }
                await TaskEx.WhenAll(tasks);
            }
        });
}
public async Task b17LoadDataStorageFileParallel17()
{
    StorageFolder data = await ApplicationData.Current.LocalFolder.GetFolderAsync("benchmarks");
    data = await data.GetFolderAsync("samplefiles");
    List<Task<Task>> tasks = new List<Task<Task>>();
    for (int i = 0; i < filepaths.Count; i++)
    {
        int index = i;
        var task = Task.Factory.StartNew<Task>(async () =>
        {
            StorageFile f = await data.GetFileAsync(filepaths[index]);
            using (var stream = await f.OpenStreamForReadAsync())
            {
                using (StreamReader r = new StreamReader(stream))
                {
                    String content = r.ReadToEnd();
                    if (content.Length == 0)
                    {
                        //just some code to ensure this is not removed by optimization from the compiler
                        //because "content" is not used otherwise
                        //should never be called
                        ShowNotificationText(content);
                    }
                }
            }
        });
        tasks.Add(task);
    }
    await TaskEx.WhenAll(tasks);
    List<Task> tasks2 = new List<Task>();
    foreach (var item in tasks)
    {
        tasks2.Add(item.Result);
    }
    await TaskEx.WhenAll(tasks2);
}

public async Task b18LoadDataStorageFileParallelThread18()
{
    StorageFolder data = await ApplicationData.Current.LocalFolder.GetFolderAsync("benchmarks");
    data = await data.GetFolderAsync("samplefiles");

    await await Task.Factory.StartNew(async () =>
    {
        List<Task<Task>> tasks = new List<Task<Task>>();
        for (int i = 0; i < filepaths.Count; i++)
        {
            int index = i;
            var task = Task.Factory.StartNew<Task>(async () =>
            {
                StorageFile f = await data.GetFileAsync(filepaths[index]);
                using (var stream = await f.OpenStreamForReadAsync())
                {
                    using (StreamReader r = new StreamReader(stream))
                    {
                        String content = r.ReadToEnd();
                        if (content.Length == 0)
                        {
                            //just some code to ensure this is not removed by optimization from the compiler
                            //because "content" is not used otherwise
                            //should never be called
                            ShowNotificationText(content);
                        }
                    }
                }
            });
            tasks.Add(task);
        }
        await TaskEx.WhenAll(tasks);
        List<Task> tasks2 = new List<Task>();
        foreach (var item in tasks)
        {
            tasks2.Add(item.Result);
        }
        await TaskEx.WhenAll(tasks2);
    });
}
public async Task b19LoadDataIsolatedStorageAsyncMyThread()
{
    using (var store = IsolatedStorageFile.GetUserStoreForApplication())
    {
        //b19
        await await Task.Factory.StartNew(async () =>
        {
            for (int i = 0; i < filepaths.Count; i++)
            {
                using (var stream = new IsolatedStorageFileStream("/benchmarks/samplefiles/" + filepaths[i], FileMode.Open, store))
                {
                    using (StreamReader r = new StreamReader(stream))
                    {
                        filecontent = await Task.Factory.StartNew<String>(() => { return r.ReadToEnd(); });
                    }
                }
            }
        });
    }
}

public async Task b20LoadDataIsolatedStorageAsyncMyConfigure()
{
    using (var store = IsolatedStorageFile.GetUserStoreForApplication())
    {
        for (int i = 0; i < filepaths.Count; i++)
        {
            using (var stream = new IsolatedStorageFileStream("/benchmarks/samplefiles/" + filepaths[i], FileMode.Open, store))
            {
                using (StreamReader r = new StreamReader(stream))
                {
                    filecontent = await Task.Factory.StartNew<String>(() => { return r.ReadToEnd(); }).ConfigureAwait(false);
                }
            }
        }
    }
}
public async Task b21LoadDataIsolatedStorageAsyncMyThreadConfigure()
{
    using (var store = IsolatedStorageFile.GetUserStoreForApplication())
    {
        await await Task.Factory.StartNew(async () =>
        {
            for (int i = 0; i < filepaths.Count; i++)
            {
                using (var stream = new IsolatedStorageFileStream("/benchmarks/samplefiles/" + filepaths[i], FileMode.Open, store))
                {
                    using (StreamReader r = new StreamReader(stream))
                    {
                        filecontent = await Task.Factory.StartNew<String>(() => { return r.ReadToEnd(); }).ConfigureAwait(false);
                    }
                }
            }
        });
    }
}
public async Task b22LoadDataOwnReadFileMethod()
{
    await await Task.Factory.StartNew(async () =>
    {
        for (int i = 0; i < filepaths.Count; i++)
        {
            filecontent = await ReadFile("/benchmarks/samplefiles/" + filepaths[i]);

        }
    });

}
public async Task b23LoadDataOwnReadFileMethodParallel()
{
    List<Task> tasks = new List<Task>();

    for (int i = 0; i < filepaths.Count; i++)
    {
        int index = i;
        var t = ReadFile("/benchmarks/samplefiles/" + filepaths[i]);
        tasks.Add(t);
    }
    await TaskEx.WhenAll(tasks);

}
public async Task b24LoadDataOwnReadFileMethodParallelThread()
{
    await await Task.Factory.StartNew(async () =>
        {
            List<Task> tasks = new List<Task>();

            for (int i = 0; i < filepaths.Count; i++)
            {
                int index = i;
                var t = ReadFile("/benchmarks/samplefiles/" + filepaths[i]);
                tasks.Add(t);
            }
            await TaskEx.WhenAll(tasks);

        });
}


public async Task b25LoadDataOwnReadFileMethodStorageFile()
{
    StorageFolder data = await ApplicationData.Current.LocalFolder.GetFolderAsync("benchmarks");
    data = await data.GetFolderAsync("samplefiles");
    await await Task.Factory.StartNew(async () =>
    {
        for (int i = 0; i < filepaths.Count; i++)
        {
            filecontent = await ReadStorageFile(data, filepaths[i]);

        }
    });

}
public async Task b26LoadDataOwnReadFileMethodParallelStorageFile()
{
    StorageFolder data = await ApplicationData.Current.LocalFolder.GetFolderAsync("benchmarks");
    data = await data.GetFolderAsync("samplefiles");
    List<Task> tasks = new List<Task>();

    for (int i = 0; i < filepaths.Count; i++)
    {
        int index = i;
        var t = ReadStorageFile(data, filepaths[i]);
        tasks.Add(t);
    }
    await TaskEx.WhenAll(tasks);

}
public async Task b27LoadDataOwnReadFileMethodParallelThreadStorageFile()
{
    StorageFolder data = await ApplicationData.Current.LocalFolder.GetFolderAsync("benchmarks");
    data = await data.GetFolderAsync("samplefiles");
    await await Task.Factory.StartNew(async () =>
    {
        List<Task> tasks = new List<Task>();

        for (int i = 0; i < filepaths.Count; i++)
        {
            int index = i;
            var t = ReadStorageFile(data, filepaths[i]);
            tasks.Add(t);
        }
        await TaskEx.WhenAll(tasks);

    });
}

public async Task b28LoadDataOwnReadFileMethodStorageFile()
{
    //StorageFolder data = await ApplicationData.Current.LocalFolder.GetFolderAsync("benchmarks");
    //data = await data.GetFolderAsync("samplefiles");
    await await Task.Factory.StartNew(async () =>
    {
        for (int i = 0; i < filepaths.Count; i++)
        {
            filecontent = await ReadStorageFile(ApplicationData.Current.LocalFolder, @"benchmarks\samplefiles\" + filepaths[i]);

        }
    });

}

public async Task<String> ReadStorageFile(StorageFolder folder, String filename)
{
    return await await Task.Factory.StartNew<Task<String>>(async () =>
    {
        String filec = "";
        StorageFile f = await folder.GetFileAsync(filename);
        using (var stream = await f.OpenStreamForReadAsync())
        {
            using (StreamReader r = new StreamReader(stream))
            {
                filec = await r.ReadToEndAsyncThread();
            }
        }
        return filec;
    });
}

public async Task<String> ReadFile(String filepath)
{
    return await await Task.Factory.StartNew<Task<String>>(async () =>
        {
            String filec = "";
            using (var store = IsolatedStorageFile.GetUserStoreForApplication())
            {
                using (var stream = new IsolatedStorageFileStream(filepath, FileMode.Open, store))
                {
                    using (StreamReader r = new StreamReader(stream))
                    {
                        filec = await r.ReadToEndAsyncThread();
                    }
                }
            }
            return filec;
        });
}

How these benchmarks are run:

public async Task RunBenchmark(String message, Func<Task> benchmarkmethod)
    {
        SystemTray.ProgressIndicator.IsVisible = true;
        SystemTray.ProgressIndicator.Text = message;
        SystemTray.ProgressIndicator.Value = 0;
        long milliseconds = 0;

        Stopwatch w = new Stopwatch();
        List<long> results = new List<long>(benchmarkruns);
        for (int i = 0; i < benchmarkruns; i++)
        {
            w.Reset();
            w.Start();
            await benchmarkmethod();
            w.Stop();
            milliseconds += w.ElapsedMilliseconds;
            results.Add(w.ElapsedMilliseconds);
            SystemTray.ProgressIndicator.Value += (double)1 / (double)benchmarkruns;
        }

        Log.Write("Fastest: " + results.Min(), "Slowest: " + results.Max(), "Average: " + results.Average(), "Median: " + results[results.Count / 2], "Maxdifference: " + (results.Max() - results.Min()),
                  "All results: " + results);


        ShowNotificationText((message + ":").PadRight(24) + (milliseconds / ((double)benchmarkruns)).ToString());
        SystemTray.ProgressIndicator.IsVisible = false;
    }

Benchmark results

Here a link to the raw benchmark data: http://www.dehodev.com/windowsphonebenchmarks.xlsx

Now the graphs (every graph shows the data for loading 50 via each method, results are all in milliseconds)

1kb file size benchmarks

The next benchmarks with 1mb are not really representative for apps. I include them here to give a better overview on how these methods scale.

enter image description here

So to sum it all up: The standard method used to read files (1.) is always the worst (except in the case you want to read 50 10mb files, but even then there are better methods).


I'm also linking this: await AsyncMethod() versus await await Task.Factory.StartNew<TResult>(AsyncMethod), where it is argued that normally it is not useful to add a new task. However the results I'm seeing here are that you just can't asume that and should always check if adding a task improves performance.

And last: I wanted to post this in the official Windows Phone developer forum but everytime I try, I get an "Unexpected Error" message...

Update 2

Conclusions:

After reviewing the data you can clearly see that no matter the file size every algorithm scales linear to the number of files. So to simplify everything we can ignore the number of files (we will just use the data for 50 files in future comparisons).

Now on to file size: File size is important. We can see that when we increase the file size the algorithms begin to converge. At 10MB file size the previous slowest algorithm takes place 4 of 8. However because this question primarily deals with phones it’s incredibly rare that apps will read multiple files with this much data, even 1MB files will be rare for most apps. My guess is, that even reading 50 20kb files is uncommon. Most apps are probably reading data in the range of 10 to 30 files, each the size of 0.5kb to 3kb. (This is only a guess, but I think it might be accurate)

Community
  • 1
  • 1
Stefan Wexel
  • 1,154
  • 1
  • 8
  • 14
  • You could try adding `ConfigureAwait(false)` to `await` statements, that might help a small bit. – Neil Turner Jul 30 '13 at 16:53
  • @Neil ConfigureAwait() is not available on Windows Phone. I will update my question in a few hours with results of extensive benchmarking and graphs. Preliminary results: `await StreamReader.ReadToEndAsync();` always is worse than the non async method. Opening a file with StorageFile has a huge overhead, but only when done in the UI thread (which totally baffles me...) – Stefan Wexel Jul 30 '13 at 17:12
  • Does it matter in which order to perform them? eg: Perform Iso first and then StorageFile? – Shawn Kendrot Jul 30 '13 at 18:11
  • @Shawn No, no difference at all. – Stefan Wexel Jul 30 '13 at 18:14
  • 1
    `ConfigureAwait()` does exist on WP, but methods must return a `Task` but I notice some of the Stream methods don't - awaiting in a loop will cause a slight perf. hit due to the constant context switching back and forth. – Neil Turner Jul 30 '13 at 18:26
  • @Neil You are correct, I was counting on Microsofts msdn documentation to be correct (http://msdn.microsoft.com/en-us/library/system.threading.tasks.task.configureawait.aspx) and only checked on an async method returning a Stream. – Stefan Wexel Jul 30 '13 at 18:33
  • @Neil When you look at my benchmark results, do you think that the reason for `await StreamReader.ReadToEndAsync()` being consistently bad is because of the constant context switching you described? For 50 files the non async needs 62ms, `await StreamReader.ReadToEndAsync()` needs 121ms. – Stefan Wexel Jul 30 '13 at 19:33
  • 1
    I would expect it to make a small difference but I don't think it's the sole reason for the perf. difference. Input from the Windows Phone team would be required to know more about the underlying implementation. – Neil Turner Jul 30 '13 at 19:49
  • Have you also tried reading the files in parallel? If it's faster or slower using the For cycle. – Martin Suchan Jul 31 '13 at 06:10
  • @Martin In my benchmarks for loading algorithms for my app I did that, noticed the difference between IsolatedStorageFile and StorageFile and created the benchmarks here to find the fastest way to load a single file. My benchmarks here are showing that, do they not? So theoretically loading the files in parallel should yield results of about the same proportions (method rankings). Continuation in next comment... – Stefan Wexel Jul 31 '13 at 10:24
  • @Martin Additionally I think that parallel loading needs to be implemented at a different level. What I mean is that when you load something you almost always want to do something like this: LoadItem() { string data = await loadfile(); return datatoitem(data); }. Now you load all items in parallel, each LoadItem() in a different thread and await TaskEx.Whenall(loaditemtasks); ------- (With loadfile() being the fastest method to load a file) However I'm going to add benchmarks with parallel loading today, just out of curiosity. – Stefan Wexel Jul 31 '13 at 10:35
  • Do you use "Debug" or "Release" mode? – jimpanzer Aug 01 '13 at 09:21
  • Release mode of course.... The benchmarks were all run with the phone not connected to the pc and after a phone restart to maximize performance – Stefan Wexel Aug 01 '13 at 10:31
  • ...i wish I could give you more votes, rep, whatever for that question alone...thanks a lot for the update! – Linky Aug 09 '13 at 23:46
  • @Linky thanks, your answer gave me some great ideas that helped me figure out answers for my questions. I'm just about to rerun some benchmarks and run some new benchmarks. When that is done, I'll answer all my questions with a detailed analysis of the data. – Stefan Wexel Aug 10 '13 at 08:35
  • @all Some question regarding the formatting of my question: Should I keep my ranking of the 8 methods from Update 2 or delete that part? It's not really representative anymore, now that I have 24 methods. I would appreciate some suggestions on what I should delete or keep. – Stefan Wexel Aug 10 '13 at 22:38
  • @jimpanzer I have no idea if the answer to my own question is sufficient for you (since that is not from a official source), and I have no idea how the bounty system works. Should I mark my answer as accepted? – Stefan Wexel Aug 10 '13 at 22:39
  • You can mark as answer any answer, even yours, but in all cases you'll not get the bounty back. – Fabske Aug 11 '13 at 08:41
  • @Fabske I know I can. The bounty is not mine, it's from jimpanzer. My questions are, does he get the bounty back if no answer is marked as accepted? When I answer my own question, do I get the bounty or does he get the bounty back? I don't want to mark my answer as accepted if I get the bounty for that if jimpanzer is not satisfied with my answer. (Because it's not an official source) – Stefan Wexel Aug 11 '13 at 10:34
  • @all: Thanks for the detailed investigation. I am on the phone team responsible for this API. We are currently investigating where this performance difference is coming from, and hopefully we will have an answer soon. – Adam Aug 19 '13 at 21:52
  • @StefanWexel It would be nice to expand Upd2 for a lazy ones :) . Okay, i agree that "Most apps are probably reading data in the range of 10 to 30 files, each the size of 0.5kb to 3kb.". What is the fastest way for reading/writing them? – Vitalii Vasylenko Apr 18 '14 at 17:05
  • @StefanWexel Ok, found your solution at the end of the answer. What about reading, did you try any benchmarking? – Vitalii Vasylenko Apr 18 '14 at 20:10
  • @Vitalii I don't know what you mean. There are hundrets of benchmark results for reading files here. No benchmarks for writing files because I think that when you have to write more than 20 files each app start/end your using a wrong app structure. Only save data that changed. – Stefan Wexel Apr 19 '14 at 16:47
  • @StefanWexel Well, image viewers (like Facebook or Tumblr apps) can be needed to store tons of files (images). Right now i'm trying to find a fastest possible solution for writing/reading to isostorage. Binary is the fastest so far ( http://mobile.dzone.com/articles/windows-phone-7-serialization-0 ), will try to apply your approach to binary reader. – Vitalii Vasylenko Apr 21 '14 at 10:15
  • @Vitalii Well yes, such apps have to store a lot of images, but normally only once when you have downloaded them. And since an image download should also be asynchrounous there is no need to save 20 images at the same time. Therefore you don't run into the same problem. Just make sure you are downloading and saving them in a secondary thread not the ui thread and you should be fine. As for loading the buffered images, take one of the solutions I described. – Stefan Wexel Apr 21 '14 at 11:53
  • @StefanWexel ok, i got that, thanks :) – Vitalii Vasylenko Apr 21 '14 at 18:02
  • @StefanWexel Ok, now i formulated a question :) So, you are saying "waits in loops are really really bad", but in the final version you're using *await ReadToEndAsyncThread { await Task }*. Wouldn't it be faster just a *await Task*? – Vitalii Vasylenko Apr 21 '14 at 18:16
  • @Vitalii Not sure what final version you are refering to. However as I wrote and you can see from the benchmarks, as long as you have the loop being done in a seperate thread you are good. I'm also not sure what you mean with _await ReadToEndAsyncThread { await Task }_, that looks weird. Though one thing I can say for sure: If you don't await both there is NO way to tell when you are done with loading your files (unless you are doing a `ContinueWith`) – Stefan Wexel Apr 21 '14 at 21:15
  • @StefanWexel in the end of your answer, you are offering to use *public static async Task ReadToEndAsyncThread(this StreamReader reader) { return await Task.Factory.StartNew(() => { return reader.ReadToEnd(); }); }* and then to call it like *filec = await r.ReadToEndAsyncThread();*. That means, it would be filec = await await Task.Factory.StartNew(); – Vitalii Vasylenko Apr 21 '14 at 21:23
  • @Vitalii Correct, do you still have a question? – Stefan Wexel Apr 22 '14 at 07:13
  • @StefanWexel Nope, now its quite clear. – Vitalii Vasylenko Apr 22 '14 at 09:01
  • @StefanWexel When you'd have some time, please, take a look at https://isostoragemanager.codeplex.com/ - i tried to create the IsoStorageManager using your approach. – Vitalii Vasylenko Apr 22 '14 at 15:56
  • @Vitalii Sorry, I don't have time to go through their source code. From the description I gather that it's very similar to a storage manager I wrote for myself. This question is a result of my storage manager performing worse for WP8 than WP7. If you have a question regarding that IsoStorageManager you should create a new question. – Stefan Wexel Apr 22 '14 at 19:34
  • Would you please post the whole benchmark app? The test methods are useless by themselves. – Euphoric May 11 '14 at 09:43
  • @Euphoric Sorry can't do that. This is not a single app but part of a lot bigger test app containing 30 more tests of private code. But why would you even need the whole app? This is literally 85% of all the code. Only the UI related thinks like button presses and creating the test data are missing. – Stefan Wexel May 11 '14 at 16:20
  • @StefanWexel There is always question if the benchmark is set up correctly. With asynchronous code, there might be case of just starting the task up and they continuing, which would make it seem that code executes awfully quick. – Euphoric May 11 '14 at 16:38
  • @Euphoric There, I added the code that runs all of the benchmarks – Stefan Wexel May 11 '14 at 16:50
  • Could you refactor those methods to accept list of files as parameter and return Task> instead of using fields? Also, it would be nice if you ensure each of those methods returns correct data. Some of the code looks like it might not be really correct. – Euphoric May 11 '14 at 18:01
  • @Euphoric What? Why? These are only test methods, why should I make them more complicated than needed? And what methods do you think are not correct? I looked over them again and they all seem fine (+ I'm pretty sure I tested all of them, when I wrote them) – Stefan Wexel May 11 '14 at 18:27

1 Answers1

15

This will be a long answer that includes answers to all my questions, and recommendations on what methods to use.

This answer is also not yet finished, but after having 5 pages in word already, I thought I'll post the first part now.


After running over 2160 benchmarks, comparing and analyzing the gathered data, I’m pretty sure I can answer my own questions and provide additional insights on how to get the best possible performance for StorageFile (and IsolatedStorageFile)

(for raw results and all benchmark methods, see question)

Let’s see the first question:

Why is await StreamReader.ReadToEndAsync() consistently slower in every benchmark than the non async method StreamReader.ReadToEnd()?

Neil Turner wrote in comments: “awaiting in a loop will cause a slight perf . hit due to the constant context switching back and forth”

I expected a slight performance hit but we both didn’t think it would cause such a big drop in every benchmark with awaits. Let’s analyze the performance hit of awaits in a loop.

For this we first compare the results of the benchmarks b1 and b5 (and b2 as an unrelated best case comparison) here the important parts of the two methods:

//b1 
for (int i = 0; i < filepaths.Count; i++)
{
    StorageFile f = await data.GetFileAsync(filepaths[i]);
    using (var stream = await f.OpenStreamForReadAsync())
    {
        using (StreamReader r = new StreamReader(stream))
        {
            filecontent = await r.ReadToEndAsync();
        }
    }
}
//b5
for (int i = 0; i < filepaths.Count; i++)
{
    StorageFile f = await data.GetFileAsync(filepaths[i]);
    using (var stream = await f.OpenStreamForReadAsync())
    {
        using (StreamReader r = new StreamReader(stream))
        {
            filecontent = r.ReadToEnd();
        }
    }
}

Benchmark results:

50 files, 100kb:

B1: 2651ms

B5: 1553ms

B2: 147

200 files, 1kb

B1: 9984ms

B5: 6572

B2: 87

In both scenarios B5 takes roughly about 2/3 of the time B1 takes, with only 2 awaits in a loop vs 3 awaits in B1. It seems that the actual loading of both b1 and b5 might be about the same as in b2 and only the awaits cause the huge drop in performance (probably because of context switching) (assumption 1).

Let’s try to calculate how long one context switch takes (with b1) and then check if assumption 1 was correct.

With 50 files and 3 awaits, we have 150 context switches: (2651ms-147ms)/150 = 16.7ms for one context switch. Can we confirm this? :

B5, 50 files: 16.7ms * 50 * 2 = 1670ms + 147ms = 1817ms vs benchmarks results: 1553ms

B1, 200 files: 16.7ms * 200 * 3 = 10020ms + 87ms = 10107ms vs 9984ms

B5, 200 files: 16.7ms * 200 * 2 = 6680ms + 87ms = 6767ms vs 6572ms

Seems pretty promising with only relative small differences that could be attributed to a margin of error in the benchmark results.

Benchmark (awaits, files): Calculation vs Benchmark results

B7 (1 await, 50 files): 16.7ms*50 + 147= 982ms vs 899ms

B7 (1 await, 200 files): 16.7*200+87 = 3427ms vs 3354ms

B12 (1 await, 50 files): 982ms vs 897ms

B12 (1 await, 200 files): 3427ms vs 3348ms

B9 (3 awaits, 50 files): 2652ms vs 2526ms

B9 (3 awaits, 200 files): 10107ms vs 10014ms

I think with this results it is safe to say, one context switch takes about 16.7ms (at least in a loop).

With this cleared up, some of the benchmark results make much more sense. In benchmarks with 3 awaits, we mostly see only a 0.1% difference in results of different file sizes (1, 20, 100). Which is about the absolute difference we can observe in our reference benchmark b2.

Conclusion: awaits in loops are really really bad (if the loop is executed in the ui thread, but I will come to that later)

On to question number 2

There seems to be a big overhead when opening a file with StorageFile, but only when it is opened in the UI thread. (Why?)

Let’s look at benchmark 10 and 19:

//b10
for (int i = 0; i < filepaths.Count; i++)
{
    using (var stream = new IsolatedStorageFileStream("/benchmarks/samplefiles/" + filepaths[i], FileMode.Open, store))
    {
        using (StreamReader r = new StreamReader(stream))
        {
            filecontent = await Task.Factory.StartNew<String>(() => { return r.ReadToEnd(); });
        }
    }
}
//b19
await await Task.Factory.StartNew(async () =>
{
    for (int i = 0; i < filepaths.Count; i++)
    {
        using (var stream = new IsolatedStorageFileStream("/benchmarks/samplefiles/" + filepaths[i], FileMode.Open, store))
        {
            using (StreamReader r = new StreamReader(stream))
            {
                filecontent = await Task.Factory.StartNew<String>(() => { return r.ReadToEnd(); });
            }
        }
    }
});

Benchmarks (1kb, 20kb, 100kb, 1mb) in ms:

10: (846, 865, 916, 1564)

19: (35, 57, 166, 1438)

In benchmark 10, we again see a huge performance hit with the context switching. However, when we execute the for loop in a different thread (b19), we get almost the same performance as with our reference benchmark 2 (Ui blocking IsolatedStorageFile). Theoretically there should still be context switches (at least to my knowledge). I suspect that the compiler optimizes the code in this situation that there are no context switches.

As a matter of fact, we get nearly the same performance, as in benchmark 20, which is basically the same as benchmark 10 but with a ConfigureAwait(false):

filecontent = await Task.Factory.StartNew<String>(() => { return r.ReadToEnd(); }).ConfigureAwait(false);

20: (36, 55, 168, 1435)

This seems to be the case not only for new Tasks, but for every async method (well at least for all that I tested)

So the answer to this question is combination of answer one and what we just found out:

The big overhead is because of the context switches, but in a different thread either no context switches occur or there is no overhead caused by them. (Of course this is not only true for opening a file as was asked in the question but for every async method)

Question 3

Question 3 can’t really be fully answered there can always be ways that might be a little bit faster in specific conditions but we can at least tell that some methods should never be used and find the best solution for the most common cases from the data I gathered:

Let’s first take a look at StreamReader.ReadToEndAsync and alternatives. For that, we can compare benchmark 7 and benchmark 10

They only differ in one line:

b7:

filecontent = await r.ReadToEndAsync();

b10:

filecontent = await Task.Factory.StartNew<String>(() => { return r.ReadToEnd(); });

You might think that they would perform similarly good or bad and you would be wrong (at least in some cases).

When I first thought of doing this test, I thought that ReadToEndAsync() would be implemented that way.

Benchmarks:

b7: (848, 853, 899, 3386)

b10: (846, 865, 916, 1564)

We can clearly see that in the case where most of the time is spent reading the file, the second method is way faster.

My recommendation:

Don’t use ReadToEndAsync() but write yourself an extension method like this:

public static async Task<String> ReadToEndAsyncThread(this StreamReader reader)
{
    return await Task.Factory.StartNew<String>(() => { return reader.ReadToEnd(); });
}

Always use this instead of ReadToEndAsync().

You can see this even more when comparing benchmark 8 and 19 (which are benchmark 7 and 10, with the for loop being executed in a different thread:

b8: (55, 103, 360, 3252)

b19: (35, 57, 166, 1438)

b6: (35, 55, 163, 1374)

In both cases there is no overhead from context switching and you can clearly see, that the performance from ReadToEndAsync() is absolutely terrible. (Benchmark 6 is also nearly identical to 8 and 19, but with filecontent = r.ReadToEnd();. Also scaling to 10 files with 10mb)

If we compare this to our reference ui blocking method:

b2: (21, 44, 147, 1365)

We can see, that both benchmark 6 and 19 come very close to the same performance without blocking the ui thread. Can we improve the performance even more? Yes, but only marginally with parallel loading:

b14: (36, 45, 133, 1074)

b16: (31, 52, 141, 1086)

However, if you look at these methods, they are not very pretty and writing that everywhere you have to load something would be bad design. For that I wrote the method ReadFile(string filepath) which can be used for single files, in normal loops with 1 await and in loops with parallel loading. This should give really good performance and result in easily reusable and maintainable code:

public async Task<String> ReadFile(String filepath)
{
    return await await Task.Factory.StartNew<Task<String>>(async () =>
        {
            String filec = "";
            using (var store = IsolatedStorageFile.GetUserStoreForApplication())
            {
                using (var stream = new IsolatedStorageFileStream(filepath, FileMode.Open, store))
                {
                    using (StreamReader r = new StreamReader(stream))
                    {
                        filec = await r.ReadToEndAsyncThread();
                    }
                }
            }
            return filec;
        });
}

Here are some benchmarks (compared with benchmark 16) (for this benchmark I had a separate benchmark run, where I took the MEDIAN (not the average) time from 100 runs of each method):

b16: (16, 32, 122, 1197)

b22: (59, 81, 219, 1516)

b23: (50, 48, 160, 1015)

b24: (34, 50, 87, 1002)

(the median in all of these is methods is very close to the average, with the average sometimes being a little bit slower, sometimes faster. The data should be comparable)

(Please note, that even though the values are the median of 100 runs, the data in the range of 0-100ms is not really comparable. E.g. in the first 100 runs, benchmark 24 had a median of 1002ms, in the second 100 runs, 899ms. )

Benchmark 22 is comparable with benchmark 19. Benchmark 23 and 24 are comparable with benchmark 14 and 16.

Ok, now this should be about one the best ways to read the files, when IsolatedStorageFile is available.

I’ll add a similar analysis for StorageFile for situations where you only have StorageFile available (sharing code with Windows 8 Apps).

And because I’m interested on how StorageFile performs on Windows 8, I’ll probably test all StorageFile methods on my Windows 8 machine too. (though for that I’m probably not going to write an analysis)

Stefan Wexel
  • 1,154
  • 1
  • 8
  • 14
  • something very interesting I found out benchmarking the StorageFile-methods on my Windows 8 PC: StorageFile seems to have automated caching. When reading 100 10mb files, 100 times, only on the first run the files are read from the SSD, the following 99 times there is virtually no activity on the SSD, but depending on the benchmark that is running, a 99% processor load. (Though even there seemingly identical methods have completely different processor loads and running times.) I'll add that, when I'm finished analyzing the results – Stefan Wexel Aug 12 '13 at 23:01
  • How much memory is RuntimeBroker consuming? I've found that running queries that return thousands of files can cause RuntimeBroker to consume >4gb of memory. – Tristan Oct 22 '14 at 05:03
  • @Tristan No clue. But since I ran the benchmarks on a Windows Phone and a Windows 8 App, memory that could be consumed before an Out OfMemoryException is thrown is very limited. I think for Windows 8 apps 250 - 400 MB is the maximum an app is allowed to consume – Stefan Wexel Oct 22 '14 at 08:56