异常会降低性能吗?

19

我的应用程序遍历目录树,在每个目录中尝试使用 File.OpenRead() 打开特定名称的文件。 如果此调用引发了 FileNotFoundException,则它知道该文件不存在。我是否应在此之前进行 File.Exists() 调用以检查文件是否存在?这样做是否更有效率?


17
异常通常用来处理意外情况,你可以使用它们来保护你的应用程序免于致命的崩溃(如空指针等)。在正常程序流程中使用它们是不良的实践。 - Yarek T
2
@Yarek 虽然这是一般的好建议,但如果你不提供替代方案,它在这里相当无用。 - CodesInChaos
2
主要问题是据我所知,除了使用本地API之外,没有非抛出式的替代方法。在这里,File.TryOpenRead会很有用。 - CodesInChaos
1
似乎不会。0xA3的评论让我意识到任何IO命令都可能失败,因此在使用FileStream时TryOpen并不那么有用。需要使用完全不同的API,如果性能很重要,使用本地(甚至是异步)IO可能更好。 - CodesInChaos
1
与win32函数相比,.NET文件函数较慢。如果速度是问题所在,那么这是我首先要查看的地方。 - Seth
显示剩余3条评论
11个回答

24

更新

我在一个循环中运行了这两个方法并计时每个方法:

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

以下是不使用调试器附加且运行发布版本的结果:

方法                  每秒迭代次数
throwException         10100
fileOpen                2200
fileExists              11300

抛出异常的成本比我预期的要高得多,而在文件不存在的情况下调用FileOpen似乎比检查不存在的文件要慢得多。

如果文件通常不存在,则使用检查文件是否存在似乎更快。相反,如果文件通常存在,则发现捕获异常更快。如果性能对您的应用程序至关重要,请在实际数据上对两种方法进行基准测试。

正如其他答案中提到的那样,即使在打开文件之前检查文件是否存在,您也应该注意竞态条件。如果有人在您的存在检查后但在您打开它之前删除了该文件,您仍然需要处理异常。


1
你的回答完全忽略了一个事实,即要打开的文件在大多数目录中可能都不存在。这种情况下,你的建议实际上会非常昂贵。 - Dirk Vollmar
4
在框架知道需要抛出异常之前,它需要完成一些工作。你是在说这些工作不是I/O工作吗? - Russell McClure
2
@Marc:说得好。实际上,从性能和清晰设计的角度来看,一个理论上不会抛出异常的File.TryOpenRead是最好的解决方案,但不幸的是,BCL团队没有提供这样一个方法。 - Ben Voigt
2
但是你所有的文件检查都是针对同一个目录的,所以缓存可能在这里起到了巨大的作用。但是由于在成功的情况下 File.Exits 和 File.Open 之间存在相同的缓存,因此它不应该改变结论。 - CodesInChaos
1
这个比较根本就没有意义。你必须比较File.Open抛出异常和File.Exists后跟随File.Open的情况。你目前只计时自己创建新异常的时间。与OP的问题相关,计时这个有什么意义呢? - Russell McClure
显示剩余12条评论

10

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

if file exists then 
    open file

如果在你检查 File.Exists 和实际打开文件之间,另一个程序删除了你的文件,那么该程序仍将引发异常。

其次,即使文件存在,也不意味着您可以实际打开该文件,您可能没有权限打开该文件,或者该文件可能是只读文件系统,因此无法以写入模式打开,等等。

文件输入/输出比异常昂贵得多,所以不需要担心异常的性能问题。

编辑: 在 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

1
在它知道需要抛出异常之前,你认为框架会做什么?你好像在表现它可以通过某种操作来发现文件不存在。 - Russell McClure
我猜这个框架只是尝试使用底层操作系统的调用打开文件(在Windows上可能是CreateFile),如果它得到一个句柄,就知道成功了,否则就知道失败了。没有竞争条件。 - dsolimano
你对并发问题的看法很准确,但基准测试可能会误导。在Python中,异常几乎是免费的 - 每个函数调用已经有传递异常的开销。而CLR可能会有不同的行为。 - Sean McSomething
@Russel McClure:不幸的是,操作系统确实有一些我们普通程序员难以轻松完成的魔法。具体来说,文件系统驱动程序可以原子地检查文件是否存在并同时打开。在多任务操作系统中,用户程序不能轻易保证file.exists后跟file.open将始终成功。 - Lie Ryan
@Lie Ryan:看一下我的答案,了解时间安排。从性能和C#风格的角度来看,调用File.Exists是正确的选择。至于在存在检查和打开之间文件消失的情况,那将是一个例外情况,当然好的程序员必须要捕捉到。但这确实是个例外情况。从我的时间安排中应该清楚,你需要先检查是否存在。 - Russell McClure

7

这种行为是否真的是例外情况呢?如果是预期的,你应该使用if语句进行测试,而不是使用异常。性能并不是这个解决方案唯一需要考虑的问题,从你试图做的事情来看,性能不应该是一个问题。因此,风格和一个好的方法应该是这个解决方案关注的重点。

因此,总结一下,既然你期望有些测试会失败,那么使用File.Exists进行检查而不是事后捕获异常。当然,你仍然应该捕获其他可能发生的异常。


IO异常是Eric Lippert所称的“外源性异常”。它绝对应该被处理。但我同意你的观点,检查应该根据文件不存在是否是正常程序执行中预期的情况来引入。 - Dirk Vollmar
4
无论如何,他都需要捕获和处理异常。首先,因为Exists会创建竞态条件(从而使代码更难理解),另外File.OpenRead可能会出现其他故障。 - CodesInChaos
非常好的答案,除了认为 File.ExistsFile.TryOpenRead 的良好替代品。它们并不相同,不能解决问题,所以不要使用它。 - Ben Voigt
1
@0xA3:不错的链接。我发现自己很恼火,因为没有一个有用的异常层次结构来区分CPU着火异常和其他异常,并且没有更多的“try”方法可用于像FileOpen这样容易失败的事情。我不喜欢使用一个笼统的“catch”来处理打开文件可能出错的情况,但是还有什么现实的替代方案呢?在vb.net中,通过一些小技巧可以捕获除了少数类型的异常(在C#中,最接近的方法我认为是捕获危险的异常并重新抛出)。有什么想法吗? - supercat
@supercat:正如我在另一条评论中提到的那样,针对IO相关内容的Try*方法根本没有意义。使用File.OpenRead时,您基本上必须处理http://msdn.microsoft.com/en-us/library/system.io.file.openread.aspx中提到的异常。您所要求的似乎是类似于已检查异常的东西,这在C#中不受支持,并且是一个非常有争议的话题。 - Dirk Vollmar
@0xA3:那么想要打开文件的每个代码都应该有一个明确的catch来处理这七个异常吗?是否更合理的做法是创建一个TryOpenRead函数,如果发生其中任何正常异常,则返回false并(可能将未抛出的异常作为Ref参数返回),同时抛出任何恶意异常? - supercat

5

进行目录搜索,找到文件后再尝试打开不是最高效的方式吗?

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

那么,您将得到一个字符串数组,您知道它们存在。

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

先运行Exists,然后再运行Open,是2个IO函数,而只尝试打开它则只有1个。因此,我认为整体性能将根据PC上的处理器时间与硬盘速度来判断。如果您的处理器较慢,我会选择检查,如果您的处理器很快,我可能会在这个问题上使用try/catch。


+1. 异常不相关 - 如果您正在遍历树,则已经知道文件是否存在(除了竞争情况外,但现在您大致知道文件(们)在哪里,这种情况不太可能发生)。使用为此特定用例设计的API只是下一个逻辑步骤 ;) - SimonJ
我会从所有目录中获取文件,然后在我进入这样的目录时检查文件是否存在于列表中。对于那些在此期间未被删除的文件,速度将非常快,并且如果需要抛出异常,则会恢复到“慢”速度。 - Daniel Mošmondor

5

这要看情况!

如果有很大的可能性文件存在(您可以针对您的场景进行判断,例如像 desktop.ini 这样的文件),我会更愿意直接尝试打开它。 但是,如果使用 File.Exist,由于并发原因和避免任何运行时异常,您需要在 try/catch 中放置 File.OpenRead,但如果文件存在的可能性较低,则可以极大地提高应用程序性能。参见鸵鸟算法


4

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

还有另一个考虑因素:调试。当您在调试器中运行时,抛出和捕获异常的成本更高,因为IDE具有钩子进入异常机制,从而增加了您的开销。如果您已经在“调试”>“异常”中勾选了任何“断开抛出”的复选框,则任何可避免的异常都会成为一个巨大的痛点。仅出于这个原因,我会主张在可能的情况下防止异常。

然而,由于时间,权限,太阳耀斑等原因,您仍然需要try-catch,这是其他答案所指出的原因。 File.Exists调用只是一种优化;它不能使您免于需要捕获异常。


好观点。调试成本可能是最相关的考虑因素。 - CodesInChaos

3

是的,你应该使用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 = 0.75秒(无论是否使用调试器)
  2. ts2 = 0.56秒(无论是否使用调试器)
  3. ts3 = 0.14秒(无论是否使用调试器)
  4. ts4 = 14.28秒(使用调试器)
  5. ts4 = 1.07秒(未使用调试器)
更新: 我添加了有无调试器的详细信息。我测试了调试和发布版本,但唯一有区别的是在使用调试器时最终抛出异常的一个函数(这很合理)。不过,仍然建议使用File.Exists进行检查。

1
你已经解释了为什么应该使用File.TryOpenRead而不是File.OpenRead。不幸的是,File.TryOpenRead实际上并不存在。而且,File.ExistsFile.TryOpenRead根本不等价。 - Ben Voigt
这让我感到困惑...在谷歌上找不到TryOpenRead的任何结果。我想,如果它存在的话...那会很有帮助吧? - Jared Updike
这个基准测试是启用调试还是禁用调试?正如其他答案所示,调试在C#中会产生巨大的影响。 - Lie Ryan
@Lie Ryan:说得好。实际上,问题不在于你使用的是调试版本还是发布版本,而在于是否连接了调试器。我会更新我的帖子,提供没有连接调试器的数字。 - Russell McClure
1
根据这个基准,对于特定的C#和.NET版本,截断点约为83%(0.56 * n + 1.07 *(1-n)= 0.75 * n + 0.14 *(1-n); n = 0.83)。如果超过83%的文件存在,则try-catch将比exists更快。但是,使用File.Exists无法解决竞态条件问题,因此,如果性能很重要,并且不存在的文件少于80%,并且你不太担心竞态条件问题,则使用file.exists;否则,如果程序的正确性和可靠性很重要,或者您预计超过80%的文件存在,则使用try-except。 - Lie Ryan

3

我不确定效率如何,但我更喜欢使用File.Exists检查。问题在于其他可能发生的事情:坏文件句柄等。如果您的程序逻辑知道有时文件不存在,并且您想要对现有和不存在的文件具有不同的行为,请使用File.Exists。如果它的缺失与其他文件相关异常相同,只需使用异常处理。


非常好的回答,除了认为 File.ExistsFile.TryOpenRead 的良好替代品。它们并不相同,不能解决问题,请不要使用它。 - Ben Voigt
@Ben:好知道,但是File.TryOpenRead不存在吗?(谷歌只有四个结果,包括这个问题。) - Jared Updike

1
我认为,一般来说,异常会“提高”系统的整体“性能”!
在您的示例中,无论如何,最好使用File.Exists...

我认为我同意,尽管这样做会有竞争条件的风险,但对于我的目的来说还可以。你为什么认为使用File.Exists更好呢? - akonsu
只有在这些文件被快速删除和重新创建的情况下,才会发生竞争条件,对吗? - Jared Updike
@Jared Updike:正是我要写的内容 :) - Lorenzo
1
@akonsu:正如其他人已经指出的那样,检查文件是否存在并不是一个异常情况... - Lorenzo
1
@Jared Updike:竞争条件与快速创建或删除无关。并发性是问题所在,即另一个进程的单个并发IO操作或某些用户交互(例如断开设备)足以触发竞争条件。 - Dirk Vollmar
@Lorenzo:我不同意。如果你从GUI文件选择器中获取文件名,那么缺少文件几乎永远不应该发生,除非发生了某些诡异的情况。 - Lie Ryan

1
使用File.Exists的问题在于它也会打开文件。因此,您最终会打开文件两次。我没有测量过,但我猜这个额外的文件打开比偶尔出现的异常更昂贵。
如果File.Exists检查提高了性能,则取决于文件存在的概率。如果它可能存在,则不要使用File.Exists,如果通常不存在,则额外的检查将提高性能。

这些异常并不是偶然发生的,因为我正在搜索文件,它们只存在于有限数量的目录中,所以我会得到大量的异常... - akonsu
那么失败的检查比成功的多吗?在这种情况下,您可以尝试测量exists是否更快。但是,您应该记录它仅是性能优化,并且即使发生竞争条件,您的代码也可以正常工作。 - CodesInChaos
2
不,File.Exists并不会打开文件。它没有创建操作系统对象来表示打开的文件句柄的额外开销——它只需要读取目录条目。而且您甚至不会因为 File.Exists 检查而两次发生这种开销,因为在此检查之后,目录条目将位于操作系统的内存缓存中。 - Joe White

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