【问题标题】:do exceptions reduce performance?异常会降低性能吗?
【发布时间】:2011-04-26 03:39:46
【问题描述】:

我的应用程序遍历目录树并在每个目录中尝试打开具有特定名称的文件(使用File.OpenRead())。如果此调用抛出FileNotFoundException,则它知道该文件不存在。我宁愿在此之前拨打File.Exists() 来检查文件是否存在吗?这样会更有效率吗?

【问题讨论】:

  • 异常是为了处理意料之外的事情,你可以用它们来保护你的应用程序免受致命的崩溃(空指针等)。在正常的程序流程中使用它们是不好的做法。
  • @Yarek 虽然这通常是个好建议,但如果你不提供替代方案,它在这里就毫无用处。
  • 主要问题是AFAIK没有替代方法(除了使用本机API)。 File.TryOpenRead 在这里很有用。
  • 好像不会。 0xA3 的评论让我意识到任何 IO 命令都可能失败,因此在使用 FileStream TryOpen 时不会那么有用。需要使用完全不同的 API,如果性能如此重要,则使用本机(甚至可能是异步)IO 可能会更好。
  • .NET 文件函数比 win32 函数慢。如果速度是问题,那是我首先要看的地方。

标签: c# .net exception-handling


【解决方案1】:

更新

我循环运行这两个方法并分别计时:

void throwException()
{
    try
    {
        throw new NotImplementedException();
    }
    catch
    {
    }
}

void fileOpen()
{
    string filename = string.Format("does_not_exist_{0}.txt", random.Next());
    try
    {
        File.Open(filename, FileMode.Open);
    }
    catch
    {
    }
}

void fileExists()
{
    string filename = string.Format("does_not_exist_{0}.txt", random.Next());
    File.Exists(filename);
}

Random random = new Random();

这些是结果没有附加调试器并运行发布版本

每秒方法迭代次数 抛出异常 10100 文件打开 2200 文件存在 11300

抛出异常的代价比我预期的要高很多,而且对不存在的文件调用 FileOpen 似乎比检查不存在的文件是否存在要慢得多。

在文件通常不存在的情况下,检查文件是否存在似乎更快。我想在相反的情况下 - 当文件通常存在时,您会发现捕获异常更快。如果性能对您的应用程序至关重要,我建议您根据实际数据对这两种方法进行基准测试。

正如其他答案中提到的,请记住,即使在打开文件之前检查文件是否存在,如果有人在您的存在检查之后但在您打开文件之前删除文件,您也应该小心竞争条件。您仍然需要处理异常。

【讨论】:

  • 您的回答完全忽略了大多数目录中可能缺少要打开的文件这一事实。在这种情况下,您的推荐实际上会非常昂贵。
  • 但是在框架知道它需要抛出异常之前,它必须做一些工作。你是说那个工作不是 I/O 工作?
  • 您能否提供更多相关信息?在没有命中异常(并且文件存在)的情况下,它肯定会更快,但如果文件存在,他可能正在读取它,这并不比文件状态慢很多,对吧? (除非该文件是一个空文件并且它的存在才是最重要的)。如果抛出异常并且堆栈展开,它不是更慢吗? (stat + stack unwind v. stat + jump)。如果您知道任何有关异常和 I/O 的文章、书籍或链接,请提供。谢谢。
  • @Marc:好点子。实际上,理论上不抛出 File.TryOpenRead 将是性能和简洁设计方面的最佳解决方案,但不幸的是 BCL 团队没有提供。
  • 但是你所有的文件检查都在同一个目录下,所以缓存在这里可能会起到很大的作用。但是由于 File.Exits 和 File.Open 之间存在相同的缓存以防万一成功,所以它不应该改变结论。
【解决方案2】:

不,不要。如果使用 File.Exists,则会引入并发问题。如果你写了这段代码:

if file exists then 
    open file

那么如果另一个程序在您检查 File.Exists 和您实际打开文件之前删除了您的文件,那么该程序仍然会抛出异常。

其次,即使一个文件存在,也不代表你真的可以打开这个文件,你可能没有打开这个文件的权限,或者这个文件可能是只读文件系统,所以你不能在其中打开写模式等

文件 I/O 比异常昂贵得多,无需担心异常的性能。

编辑: Linux 下 Python 中的基准测试异常与存在

import timeit
setup = 'import random, os'

s = '''
try:
    open('does not exist_%s.txt' % random.randint(0, 10000)).read()
except Exception:
    pass
'''
byException = timeit.Timer(stmt=s, setup=setup).timeit(1000000)

s = '''
fn = 'does not exists_%s.txt' % random.randint(0, 10000)
if os.path.exists(fn):
    open(fn).read()
'''
byExists = timeit.Timer(stmt=s, setup=setup).timeit(1000000)

print 'byException: ', byException   # byException:  23.2779269218
print 'byExists: ', byExists  # byExists:  22.4937438965

【讨论】:

  • 你认为框架在知道它需要抛出异常之前会做什么?你表现得好像它可以神奇地运行一些 noop 并知道该文件不存在。
  • 我猜框架只是尝试使用底层操作系统的调用打开文件(可能在 Windows 上为CreateFile,如果它得到一个句柄,它就知道它成功了,否则它知道它失败了。没有竞争条件。
  • 您发现了并发问题,但基准可能具有误导性。 Python 中的异常几乎是免费的——每个函数调用都已经有传递异常的开销。 CLR 可能有不同的行为。
  • @Russel McClure:不幸的是,操作系统确实有一些我们普通程序员无法轻易做到的魔力。具体来说,文件系统驱动程序可以自动检查文件是否存在并同时打开。在多任务操作系统中,用户程序不能轻易保证 file.exists 后跟 file.open 总是成功。
  • @Lie Ryan:看看我对时间的回答。从性能角度和 C# 风格角度来看,调用 File.Exists 都是正确的选择。至于在存在检查和打开之间消失的文件,那将是一个例外情况,当然一个好的程序员必须抓住。但这确实是一个特例。从我的时间安排来看,您应该首先检查是否存在。
【解决方案3】:

这种行为真的异常吗?如果是预期的,您应该使用 if 语句进行测试,并且根本不使用异常。性能不是此解决方案的唯一问题,从您尝试做的声音来看,性能应该不是问题。因此,风格和好的方法应该是这个解决方案关注的项目。

因此,总而言之,由于您预计某些测试会失败,因此请使用 File.Exists 进行检查,而不是在事后捕获异常。当然,您仍然应该捕获其他可能发生的异常。

【讨论】:

  • 一个 IO 异常是 Eric Lippert 所说的 exogenous exception。绝对应该处理。但我同意你的观点,应该根据常规程序执行是否预期文件不存在来引入检查。
  • 他无论如何都需要捕获和处理异常。一方面是因为 Exists 创建了一个竞争条件(从而使代码更难理解),并且 File.OpenRead 还有其他方法失败。
  • 很好的答案,除了认为File.Exists 可以很好地替代File.TryOpenRead。不一样,解决不了问题,别用了。
  • @0xA3:不错的链接。我发现自己非常恼火的是,没有一个有用的异常层次结构可以将 CPU-on-fire 异常与其他异常区分开来,并且没有更多的“尝试”方法可用于像 FileOpen 这样容易失败的东西。我不喜欢对打开文件可能出错的东西使用毯子“catch”,但有什么现实的替代方案呢?在 vb.net 中,除了少数几种类型的异常外,可以通过一些小技巧来捕获所有异常(在 C# 中,我认为最接近的异常是捕获危险的异常并重新抛出)。有什么想法吗?
  • @supercat:正如我在另一条评论中已经提到的,用于 IO 相关内容的 Try* 方法根本没有意义。使用File.OpenRead,您基本上必须处理msdn.microsoft.com/en-us/library/system.io.file.openread.aspx 中提到的异常。你所要求的似乎是 C# 不支持的检查异常之类的东西,而且这个话题很有争议。
【解决方案4】:

视情况而定!

如果文件存在的可能性很高(您知道这一点适合您的场景,但例如desktop.ini),我宁愿直接尝试打开它。 无论如何,如果使用File.Exist,出于并发原因并避免任何运行时异常,您需要将File.OpenRead 放入try/catch 中,但如果文件存在的机会很低,它将大大提高您的应用程序性能。 Ostrich algorithm

【讨论】:

    【解决方案5】:

    运行目录搜索,找到它,然后尝试打开它不是最有效的吗?

    Dim Files() as string = System.IO.Directory.GetFiles("C:\", "SpecificName.txt", IO.SearchOption.AllDirectories)
    

    然后你会得到一个你知道存在的字符串数组。

    哦,作为对原始问题的回答,我会说是的,try/catch 会引入更多的处理器周期,我还假设 IO peeks 实际上花费的时间比处理器周期的开销要长。

    首先运行 Exists,然后运行第二个,是 2 个 IO 函数,而 1 个只是试图打开它。所以说真的,我想说整体性能将是对处理器时间与它将运行的 PC 上的硬盘驱动器速度的判断。如果你有一个较慢的处理器,我会检查,如果你有一个快速的处理器,我可能会在这个上使用 try/catch。

    【讨论】:

    • +1。例外情况无关紧要 - 如果您正在遍历树,那么您已经知道文件是否存在(在比赛场景中除外,但现在您大致知道文件在哪里的可能性要小得多) .使用专为这个确切的用例设计的 API 只是下一个合乎逻辑的步骤 ;)
    • 当我在这样的目录中时,我会从所有目录中获取文件,然后检查文件是否存在列表。对于同时没有被删除的文件,它会很快,如果有异常被抛出,它会恢复到“慢”的速度。
    【解决方案6】:

    File.Exists 是很好的第一道防线。如果该文件不存在,那么如果您尝试打开它,您肯定会遇到异常。存在性检查比抛出和捕获异常的成本要低。 (也许不会便宜多少,但有点。)

    还有另一个考虑因素:调试。当您在调试器中运行时,抛出和捕获异常的成本更高,因为 IDE 有挂钩到异常机制,从而增加了您的高架。如果您在 Debug > Exceptions 中选中了任何“Break on throw”复选框,那么任何可避免的异常都会成为一个巨大的痛点。仅出于这个原因,我会主张尽可能防止异常。

    但是,由于此处其他答案指出的原因,您仍然需要 try-catch。 File.Exists 调用仅仅是一种优化;由于时间、权限、太阳耀斑等原因,它并不能帮助您避免捕获异常。

    【讨论】:

    • 好点。调试成本可能应该是最相关的考虑因素。
    【解决方案7】:

    我不知道效率,但我更喜欢 File.Exists 检查。问题在于可能发生的所有其他事情:错误的文件句柄等。如果您的程序逻辑知道有时该文件不存在,并且您希望对现有文件和不存在的文件有不同的行为,请使用 File.存在。如果它的不存在和其他文件相关的异常一样,使用异常处理即可。

    【讨论】:

    • 非常好的答案,除了认为File.Exists 可以很好地替代File.TryOpenRead。不一样,解决不了问题,别用了。
    • @Ben:很高兴知道,除了 File.TryOpenRead 不存在? (谷歌有四个点击,包括这个问题。)
    【解决方案8】:

    是的,您应该使用 File.Exists。异常应该用于不控制程序正常流程的异常情况。在您的情况下,文件不存在并不是异常事件。因此,您不应依赖异常。

    更新:

    所以每个人都可以自己尝试一下,我将发布我的测试代码。对于不存在的文件,依靠 File.Open 为您抛出异常比使用 File.Exists 检查差大约 50 倍。

    class Program
    {
       static void Main(string[] args)
       {
          TimeSpan ts1 = TimeIt(OpenExistingFileWithCheck);
    
          TimeSpan ts2 = TimeIt(OpenExistingFileWithoutCheck);
    
          TimeSpan ts3 = TimeIt(OpenNonExistingFileWithCheck);
    
          TimeSpan ts4 = TimeIt(OpenNonExistingFileWithoutCheck);
       }
    
       private static TimeSpan TimeIt(Action action)
       {
          int loopSize = 10000;
    
          DateTime startTime = DateTime.Now;
          for (int i = 0; i < loopSize; i++)
          {
             action();
          }
    
          return DateTime.Now.Subtract(startTime);
       }
    
       private static void OpenExistingFileWithCheck()
       {
          string file = @"C:\temp\existingfile.txt";
          if (File.Exists(file))
          {
             using (FileStream fs = File.Open(file, FileMode.Open, FileAccess.Read))
             {
             }
          }
       }
    
       private static void OpenExistingFileWithoutCheck()
       {
          string file = @"C:\temp\existingfile.txt";
          using (FileStream fs = File.Open(file, FileMode.Open, FileAccess.Read))
          {
          }
       }
    
       private static void OpenNonExistingFileWithCheck()
       {
          string file = @"C:\temp\nonexistantfile.txt";
          if (File.Exists(file))
          {
             using (FileStream fs = File.Open(file, FileMode.Open, FileAccess.Read))
             {
             }
          }
       }
    
       private static void OpenNonExistingFileWithoutCheck()
       {
          try
          {
             string file = @"C:\temp\nonexistantfile.txt";
             using (FileStream fs = File.Open(file, FileMode.Open, FileAccess.Read))
             {
             }
          }
          catch (Exception ex)
          {
          }
       }
    }
    

    在我的电脑上:

    1. ts1 = .75 秒(连接或不连接调试器时相同)
    2. ts2 = .56 秒(连接或不连接调试器时相同)
    3. ts3 = .14 秒(连接或不连接调试器时相同)
    4. ts4 = 14.28 秒(附加调试器)
    5. ts4 = 1.07(未附加调试器)

    更新:

    我添加了有关是否附加了配音器的详细信息。我测试了调试和发布版本,但唯一不同的是在附加调试器时最终抛出异常的一个函数(这是有道理的)。尽管如此,检查 File.Exists 是最好的选择。

    【讨论】:

    • 您已经解释了为什么应该使用File.TryOpenRead 而不是File.OpenRead。不幸的是 File.TryOpenRead 实际上并不存在。而且File.Exists 根本不等同于File.TryOpenRead
    • 这让我很困惑……在 Google 上找不到 TryOpenRead 的任何点击量。我想如果它存在……会有所帮助吗?
    • 这个基准测试是启用还是禁用调试?正如其他答案所示,调试在 C# 中有很大的不同。
    • @Lie Ryan:好点子。实际上,这与您是否具有调试版本或发布版本无关,而是是否附加了调试器。我将在没有附加调试器的情况下使用数字更新我的帖子。
    • 从此基准推断,对于特定的 C# 和 .NET 版本,截止点约为 83% (0.56 * n + 1.07 * (1 - n) = 0.75 * n + 0.14 * ( 1 - n);n = 0.83)。如果存在超过 83% 的文件,则 try-catch 将比存在更快。但是,使用 File.Exists 并不能解决竞态条件,所以如果性能很重要,你有不到 80% 的文件不存在,而且你不太担心竞态条件,然后使用 file.exists;否则,如果程序的正确性和可靠性很重要,或者如果您预计存在超过 80% 的文件,则使用 try-except。
    【解决方案9】:

    我想说,一般来说,异常会“提高”系统的整体“性能”!

    无论如何,在您的示例中,最好使用 File.Exists...

    【讨论】:

    • 我想我同意,虽然如果我这样做我会冒着进入比赛条件的风险,但就我的目的而言,这没关系。为什么你认为使用 File.Exists 更好?
    • 竞争条件只有在这些文件被快速删除和重新创建时才会发生,对吧?
    • @Jared Updike:正是我写的:)
    • @akonsu:仅仅是因为,正如其他人已经指出的那样,检查文件是否存在并不是一种特殊情况......
    • @Jared Updike:竞争条件与 快速 创建或删除无关。 并发是问题,即另一个进程的单个并发IO操作或某些用户交互(例如断开设备连接)足以触发竞争条件。
    【解决方案10】:

    首先使用 File.Exists 的问题是它也会打开文件。所以你最终打开文件两次。我没有测量过,但我猜这个文件的额外打开比偶尔的例外更昂贵。

    如果 File.Exists 检查提高性能取决于文件存在的概率。如果它可能存在,则不要使用 File.Exists,如果它通常不存在,则附加检查将提高性能。

    【讨论】:

    • 好吧,这些异常不是偶然的,因为我正在搜索文件并且它们只存在于有限数量的目录中,所以我得到了大量的异常......
    • 所以检查失败多于成功?在这种情况下,您可以尝试测量是否存在更快。但是您应该证明这只是一种性能优化,即使出现竞争条件,您的代码也能正常工作。
    • 不,File.Exists 不会打开文件。创建一个 OS 对象来表示一个打开的文件句柄没有任何开销——它只需要读取目录条目。而且您甚至不会产生两次成本,因为在 File.Exists 检查之后,目录条目将在操作系统的内存缓存中。
    【解决方案11】:

    异常的开销很明显,但与文件操作相比并不显着。

    【讨论】:

      猜你喜欢
      • 2018-12-12
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-10-20
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多