StorageFile比IsolatedStorageFile慢50倍

28
我刚在测试多个算法,以找到加载应用程序中所有数据的最快方式时,发现我的Lumia 920上运行的WP7版本的应用程序比在同一设备上运行的WP8版本加载数据快2倍。 随后,我编写了以下独立代码来测试WP8的StorageFile和WP7的IsolatedStorageFile的性能。要澄清标题,这是我进行的初步基准测试结果,读取50个20kb和100kb的文件:(插入图片) 关于代码,请参见下面 更新 经过今天几个小时的基准测试和一些有趣的结果,让我重新提出问题: 1.为什么在每个基准测试中,await StreamReader.ReadToEndAsync()始终比非异步方法StreamReader.ReadToEnd()慢? 2.使用StorageFile打开文件时存在很大的开销,但仅当它在UI线程中打开时才会出现。 3.是否有其他读取文件的方法可能更快?更新3 添加了10个算法,重新运行了每个算法,每个先前使用的文件大小和文件数量都进行了测试。这次每个算法运行了10次。因此,在Excel文件中的原始数据是这些运行的平均值。由于现在有18种算法,每种算法都使用4个文件大小(1kb,20kb,100kb,1mb)进行测试,每个大小都有50,100和200个文件(18 * 4 * 3 = 216),因此总共进行了2160个基准测试运行,用时95分钟(原始运行时间)。 更新5 添加了基准测试25、26和27以及ReadStorageFile方法。 必须删除一些文本,因为该帖子的字符超过了30,000个,这显然是最大值。使用新数据、新结构、比较和新图形更新了Excel文件。关于代码请参见上文。
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;
        });
}

这些基准测试的运行方式如下:
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;
    }

基准测试结果

这里是原始基准数据的链接:http://www.dehodev.com/windowsphonebenchmarks.xlsx

现在看图表(每个图表显示了通过每种方法加载50次的数据,结果都以毫秒为单位)

1kb文件大小基准测试

下一个1mb的基准测试并不真正代表应用程序。我将它们包含在这里,以便更好地了解这些方法如何扩展。

输入图像描述

所以总而言之:用于读取文件的标准方法(1.)始终是最差的(除非您想要读取50个10mb文件,但即使在这种情况下,也有更好的方法)。


我还链接了这个:await AsyncMethod() versus await await Task.Factory.StartNew<TResult>(AsyncMethod),其中认为通常添加新任务没有用处。然而,我在这里看到的结果是你不能假设,应该始终检查是否添加任务可以提高性能。

最后:我想在官方Windows Phone开发者论坛上发布此内容,但每次尝试时都会收到“意外错误”消息...

更新2

结论:

经过审查数据,您可以清楚地看到无论文件大小如何,每个算法都是线性扩展到文件数量。因此,为了简化一切,我们可以忽略文件数量(我们将仅使用50个文件的数据进行未来比较)。

现在讨论文件大小:文件大小很重要。当我们增加文件大小时,算法开始收敛。在10MB文件大小时,前一个最慢的算法占据8个中的4个位置。然而,由于这个问题主要涉及手机,应用程序读取具有这么多数据的多个文件非常罕见,即使是1MB文件对于大多数应用程序也很少见。我的猜测是,即使读取50个20kb的文件也不常见。大多数应用程序可能正在读取0.5kb到3kb大小范围内的数据。(这只是一个猜测,但我认为可能是准确的)


1
ConfigureAwait() 在 WP 上是存在的,但方法必须返回一个 Task,但我注意到一些 Stream 方法不返回 Task。在循环中等待会导致轻微的性能损失,因为不断地上下文切换。 - Neil Turner
1
我认为这会产生一些微小的差异,但我不认为这是性能差异的唯一原因。需要来自Windows Phone团队的输入才能更多地了解底层实现。 - Neil Turner
@所有人:感谢详细的调查。我是负责该API的电话团队。我们正在调查性能差异来自何处,希望很快能得出答案。 - Adam
请问您能否发布整个基准测试应用程序?单独的测试方法是没有意义的。 - Euphoric
@Euphoric 抱歉,无法做到。这不是一个单独的应用程序,而是包含30个以上私有代码测试的更大的测试应用程序的一部分。但是你为什么需要整个应用程序呢?这实际上是所有代码的85%。只有与UI相关的内容,例如按钮按下和创建测试数据缺失。 - Stefan Wexel
显示剩余34条评论
1个回答

15

这将是一个长答案,包括对所有问题的回答以及对使用何种方法的建议。

这个答案还没有完成,但在Word中已经有了5页,我想现在先发布第一部分。


在运行超过2160个基准测试、比较和分析收集的数据之后,我非常确定我可以回答自己的问题,并提供有关如何获得StorageFile(和IsolatedStorageFile)最佳性能的额外见解。

(有关原始结果和所有基准测试方法,请参阅问题)

让我们看看第一个问题:

为什么每次基准测试中 await StreamReader.ReadToEndAsync() 的速度都比非异步方法 StreamReader.ReadToEnd() 慢?

尼尔·特纳在评论中写道:“在循环中等待会导致轻微的性能下降,因为需要不断地来回上下文切换”

我预计会有轻微的性能下降,但我们两个都没想到它会在每个带有 await 的基准测试中导致如此大的下降。让我们分析在循环中等待的性能损失。

为此,我们首先比较基准测试 b1 和 b5 的结果(以及 b2 作为不相关的最佳情况比较),这里是两种方法的重要部分:

//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();
        }
    }
}

基准测试结果:

50个文件,每个100kb:

B1:2651ms

B5:1553ms

B2:147ms

200个文件,每个1kb:

B1:9984ms

B5:6572ms

B2:87ms

在这两种情况中,B5所需的时间大约是B1所需时间的2/3,尽管B1中有3个等待,而B5只有2个等待。似乎实际加载B1和B5的时间可能与B2相同,只有等待导致性能大幅下降(可能是因为上下文切换)(假设1)。

让我们尝试计算一个上下文切换需要多长时间(使用B1),然后检查假设1是否正确。

对于50个文件和3个等待,我们有150个上下文切换:(2651ms-147ms)/ 150 =每个上下文切换16.7ms。我们可以证实吗? :

B5,50个文件:16.7ms * 50 * 2 = 1670ms + 147ms = 1817ms,而基准测试结果为:1553ms

B1,200个文件:16.7ms * 200 * 3 = 10020ms + 87ms = 10107ms,而基准测试结果为9984ms

B5,200个文件:16.7ms * 200 * 2 = 6680ms + 87ms = 6767ms,而基准测试结果为6572ms

只有相对较小的差异,可以归因于基准测试结果的误差限,看起来非常有前途。

基准测试(等待,文件):计算与基准测试结果

B7(1个等待,50个文件):16.7ms * 50 + 147 = 982ms,而基准测试结果为899ms

B7(1个等待,200个文件):16.7 * 200 + 87 = 3427ms,而基准测试结果为3354ms

B12(1个等待,50个文件):982ms,而基准测试结果为897ms

B12(1个等待,200个文件):3427ms,而基准测试结果为3348ms

B9(3个等待,50个文件):2652ms,而基准测试结果为2526ms

B9(3个等待,200个文件):10107ms,而基准测试结果为10014ms

我认为根据这些结果可以放心地说,一个上下文切换需要大约16.7ms(至少在循环中)。

弄清楚这一点后,一些基准测试结果就更加有意义了。在带有3个等待的基准测试中,我们通常只看到不同文件大小(1、20、100)之间的0.1%差异。这大约是我们可以在参考基准测试b2中观察到的绝对差异。

结论:循环中的等待非常糟糕(如果在ui线程中执行循环,但稍后我会解释)

转到第二个问题

使用StorageFile打开文件时似乎存在很大的开销,但仅当在UI线程中打开文件时才会出现。(为什么?)

让我们看一下基准测试10和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(); });
            }
        }
    }
});

基准测试 (1kb, 20kb, 100kb, 1mb) 的时间(单位:毫秒):

10: (846, 865, 916, 1564)

19: (35, 57, 166, 1438)

从基准测试10的结果可以看出,上下文切换会严重影响性能。但是,当我们在不同的线程(b19)中执行for循环时,与我们参考的基准测试2(Ui blocking IsolatedStorageFile)的性能几乎相同。理论上仍然应该有上下文切换(至少根据我的了解)。我怀疑编译器在这种情况下优化了代码,使得没有上下文切换。

事实上,在benchmark 20中,我们几乎得到了与benchmark 10相同的性能,benchmark 20和benchmark 10基本相同,只是加了一个ConfigureAwait(false):

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

20: (36, 55, 168, 1435)

这似乎不仅适用于新任务,而且对于每个异步方法都是如此(至少对我测试过的所有方法都是如此)

所以这个问题的答案是回答一和我们刚发现的内容的结合:

大的开销是由于上下文切换引起的,但在不同的线程中要么不会发生上下文切换,要么它们不会引起任何开销。(当然,这不仅适用于在问题中问到的打开文件,而是适用于每个异步方法)

问题3

问题3实际上无法完全回答,总会有在特定条件下可能略微更快的方法,但我们至少可以说出某些方法不应该使用,并从我收集的数据中找到最常见情况的最佳解决方案:

首先让我们看一下 StreamReader.ReadToEndAsync 和替代方法。为此,我们可以比较基准7和基准10

它们只在一行上有所不同:

b7:

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

你可能认为它们的表现相似,但你错了(至少在某些情况下)。

当我第一次想做这个测试时,我认为ReadToEndAsync()会以那种方式实现。

基准测试:

b7: (848, 853, 899, 3386)

b10: (846, 865, 916, 1564)

我们可以清楚地看到,在大部分时间用于读取文件的情况下,第二种方法要快得多。

我的建议:

不要使用ReadToEndAsync(),而是编写一个像这样的扩展方法:

public static async Task<String> ReadToEndAsyncThread(this StreamReader reader)
{
    return await Task.Factory.StartNew<String>(() => { return reader.ReadToEnd(); });
}
始终使用它来代替ReadToEndAsync()
当比较基准测试8和19时(基准测试7和10执行了不同线程中的for循环),您可以更清楚地看到这一点:
b8:(55、103、360、3252)
b19:(35、57、166、1438)
b6:(35、55、163、1374)
在两种情况下都没有上下文切换的开销,并且您可以清楚地看到ReadToEndAsync()的性能非常糟糕。(基准测试6也几乎与8和19相同,但带有 filecontent = r.ReadToEnd() 。还缩放到具有10mb的10个文件)
如果我们将其与参考UI阻止方法进行比较:
b2:(21、44、147、1365)
我们可以看到,基准测试6和19都可以在不阻塞UI线程的情况下非常接近相同的性能。我们可以进一步提高性能吗?是的,但只有稍微用并行加载:
b14:(36、45、133、1074)
b16:(31、52、141、1086)
但是,如果您查看这些方法,它们并不太漂亮,并且在每次加载内容时都要编写这些内容会导致糟糕的设计。因此,我编写了方法ReadFile(string filepath),可以用于单个文件,在正常循环中带有1个等待以及在循环中具有并行加载。这应该提供非常好的性能并导致易于重用和维护的代码:
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;
        });
}

以下是一些基准测试结果(与基准测试16进行比较)(针对此基准测试,我进行了单独的基准测试运行,在其中从每种方法的100次运行中取出MEDIAN(而不是平均)时间):

b16: (16, 32, 122, 1197)

b22: (59, 81, 219, 1516)

b23: (50, 48, 160, 1015)

b24: (34, 50, 87, 1002)

(所有这些方法中的中位数都非常接近平均值,平均值有时会慢一点,有时会快一点。数据应该是可比较的)

(请注意,即使这些值是100次运行的中位数,范围在0-100ms内的数据也不是真正可比较的。例如,在第一组100次运行中,基准测试24的中位数为1002ms,在第二组100次运行中则为899ms。)

基准测试22与基准测试19相当。基准测试23和24可与基准测试14和16相比较。

好的,现在这应该是使用IsolatedStorageFile读取文件的最佳方式之一。

当只有StorageFile可用时(与Windows 8应用程序共享代码的情况),我将为StorageFile添加类似的分析。

因为我对Windows 8上的StorageFile表现很感兴趣,所以我可能也会在我的Windows 8机器上测试所有StorageFile方法。(虽然我可能不会写分析)


在我的Windows 8电脑上对StorageFile方法进行基准测试时,我发现了一些非常有趣的事情:StorageFile似乎具有自动缓存功能。当读取100个10MB文件100次时,仅在第一次运行时文件从SSD中读取,接下来的99次几乎没有任何SSD活动,但根据正在运行的基准测试,处理器负载为99%。(尽管即使在那里,看似相同的方法也具有完全不同的处理器负载和运行时间。)我会补充说,在我完成分析结果之后。 - Stefan Wexel
RuntimeBroker消耗了多少内存?我发现运行返回数千个文件的查询可能会导致RuntimeBroker消耗超过4GB的内存。 - Tristan
@Tristan 不清楚。但是由于我在Windows Phone和Windows 8应用程序上运行了基准测试,因此可以消耗的内存量在引发OutOfMemoryException之前非常有限。我认为对于Windows 8应用程序,一个应用程序被允许消耗的最大内存为250-400 MB。 - Stefan Wexel

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接