Jump to content
dummzeuch

What is the fastest way to check if a file exists?

Recommended Posts

When searching for "windows fastest way to check if a file exists" I get lots of hits that claim there is no API for it and one should use GetFileAttributes or FindFirst instead. But there is PathFileExistsA which according to the description does exactly that: Check if a file exists. It has been around since Windows 2000.

 

Yet, FileExists in System.Sysutils still uses GetFileAttributes.

 

Am I missing something? Is maybe GetFileAttributes actually faster than PathFileExistsA ?

Edited by dummzeuch
  • Like 1

Share this post


Link to post

FindFirst is definitively the slowest. It needs at least two calls (+ FindClose), and maintain some state in-between.
 

GetFileAtributes() is the fastest under Windows, and fpaccess/euidaccess on POSIX systems.
Note that a fallback to FileAge() is needed in case of sharing violation:
 

function FileExists(const FileName: string): Boolean;
{$IFDEF MSWINDOWS}
// use GetFileAttributes: much faster than original, which uses FileAge=FindFirst
var Attr: Integer;
    LastError: Cardinal;
begin
    Attr := Integer(GetFileAttributesW(pointer(FileName)));
  if Attr <> -1 then
    Result := Attr and FILE_ATTRIBUTE_DIRECTORY = 0 else begin
    LastError := GetLastError;
    Result := (LastError <> ERROR_FILE_NOT_FOUND) and
              (LastError <> ERROR_PATH_NOT_FOUND) and
              (LastError <> ERROR_INVALID_NAME) and
              // (use FileAge to test SHARE_EXCLUSIVE files)
              ((LastError = ERROR_SHARING_VIOLATION) or (FileAge(FileName)<>-1));
  end;
end;
{$ENDIF}
{$IFDEF LINUX}
begin
  Result := euidaccess(PChar(FileName), F_OK) = 0;
end;
{$ENDIF}

(extracted from our Enhanced RTL source code)

 

But the version currently included in Delphi 10.3 Rio SysUtils.pas is just as fast on Windows.
On POSIX, it uses stats, which is slower than euidaccess().

  • Like 4
  • Thanks 3

Share this post


Link to post
15 hours ago, Arnaud Bouchez said:

 

...(extracted from our Enhanced RTL source code)

 

But the version currently included in Delphi 10.3 Rio SysUtils.pas is just as fast on Windows.
On POSIX, it uses stats, which is slower than euidaccess().

Does that work with UNC paths? (Just asking)

Share this post


Link to post

Hello again, you could also try with PIDL, i really dont know whats faster, FileAge() or something like that: (not optimized and just for local files...)

function SHGetIDListFromPath(Path: TFileName; var ShellFolder: IShellFolder): PItemIDList; 
var 
  TempPath, NextDir: TFileName; 
  SlashPos: Integer; 
  Folder, subFolder: IShellFolder; 
  PIDL, PIDLbase: PItemIDList; 
  ParseStruct: TStrRet; 
  ParseNAme: String; 
  EList: IEnumIDList; 
  DidGet: Cardinal; 
  ScanParam: Integer; 
begin 
SHGetDesktopFolder(Folder); 
SHGetSpecialFolderLocation(0, CSIDL_DRIVES, PIDLbase); 

OLECheck(Folder.BindToObject(PIDLbase, nil, IID_IShellFolder, Pointer(SubFolder))); 
TempPath:=Path; 
NextDir:=''; 

while Length(TempPath)>0 do 
  begin 
  SlashPos:=Pos('\', TempPath); 
  if SlashPos > 0 then 
    begin 
    if Pos(':', TempPath) > 0 then NextDir:=Copy(TempPath, 1, 3) 
      else NextDir:=SlashDirName(NextDir)+Copy(TempPath, 1, SlashPos-1); 
    TempPath:=Copy(TempPath, SlashPos+1, Length(TempPath)); 
    end 
  else 
    begin 
    if NextDir='' then NextDir:=TempPath 
      else NextDir:=SlashDirName(NextDir)+TempPath; 
    TempPath:=''; 
    end; 
  PIDL:=PidlBase; 
  ScanParam:=SHCONTF_FOLDERS or SHCONTF_INCLUDEHIDDEN; 
  if (NextDir=Path) and (not DirectoryExists(Path)) then 
    ScanParam:=ScanParam or SHCONTF_NONFOLDERS; 

  if S_OK=SubFolder.EnumObjects(0, ScanParam, EList) then 
    while S_OK=EList.Next(1, pidl, DidGet) do 
      begin 
      OLECheck(SubFolder.GetDisplayNameOf(PIDL, SHGDN_FORPARSING, ParseStruct)); 
      case ParseStruct.uType of 
        STRRET_CSTR: ParseName:=ParseStruct.cStr; 
        STRRET_WSTR: ParseName:=WideCharToString(ParseStruct.pOleStr); 
        STRRET_OFFSET: Parsename:=PChar(DWORD(Pidl)+ParseStruct.uOffset); 
        end; 
      if UpperCase(Parsename)=UpperCase(NextDir) then Break; 
      end 
    else 
      begin 
      Folder:=nil; 
      Result:=nil; 
      Exit; 
      end; 

  if DidGet=0 then 
    begin 
    Folder:=nil; 
    Result:=nil; 
    Exit; 
    end; 
  PIDLBase:=PIDL; 
  Folder:=subFolder; 

  if not FileExists(NextDir) then 
    OLECheck(Folder.BindToObject(Pidl, nil, IID_IShellFolder, Pointer(SubFolder))); 
  end; 
ShellFolder:=Folder; 
if ShellFolder=nil then Result:=nil 
  else Result:=PIDL; 
end; 

I welcome to see some quality Benchmark Results.

Share this post


Link to post

FileAge uses GetFileAttributesEx internally, which in my tests was slower than GetFileAttributes which the RTL uses in FileExists.

Share this post


Link to post
uses
 ActiveX, Shlobj, IOUtils;

function TestFileExists( Filename: String ): Boolean;
begin
  Result := FileExists( Filename );
end;

type
  TParseDisplayName = function(pszPath: PWideChar; pbc: IBindCtx; var pidl: PItemIDList; sfgaoIn: ULong; var psfgaoOut: ULong): HResult; stdcall;

var
  SHParseDisplayName: TParseDisplayName;
  SHELL32DLLHandle : THandle;

function TestPIDL( Filename: String ): PItemIdList;
var
  PIDL: PItemIdList;
  Attrs: DWORD;
begin
  Result := nil;
  try
    CoInitialize(nil);
    if ( SHParseDisplayName( PChar( Filename ), nil, PIDL, 0, Attrs ) = S_OK ) then
      if Assigned( PIDL ) then
        Result := PIDL;
  finally
    CoUnInitialize();
  end;
end;

function TestCreateFile( Filename: String ): Boolean;
var
  hFile : THandle;
begin
  Result := False;
  hFile := CreateFile(PChar( Filename ), GENERIC_READ, FILE_SHARE_READ, nil, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
  if hFile <> INVALID_HANDLE_VALUE then
    begin
      Result := True;
      CloseHandle(hFile);
    end;
end;

function TestGetFileAtrributes( Filename: String ): Boolean;
var
  i: Cardinal;
begin
  Result := False;
  i := GetFileAttributes( PChar( Filename ) );
  if i <> INVALID_FILE_ATTRIBUTES then
    begin
      Result := True;
    end;
end;

function TestFileAge( Filename: String ): Boolean;
var
 DT: TDateTime;
begin
  Result := False;
  if FileAge( Filename, DT ) then
    begin
      Result := True;
    end;
end;

function TestFileGetAttr( Filename: String ): Boolean;
begin
  Result := False;
  if FileGetAttr( Filename ) <> 0 then
    begin
      Result := True;
    end;
end;

function TestTFile( Filename: String ): Boolean;
begin
  Result := False;
  if TFile.Exists( Filename ) then
    begin
      Result := True;
    end;
end;

function TestFindFirst( Filename: String ): Boolean;
var
  sr: TSearchRec;
begin
  Result := False;
  if FindFirst( Filename, faAnyFile, sr ) = 0 then
    begin
      Result := True;
    end;
  FindClose(sr);
end;


procedure TForm1.btnDoJobClick(Sender: TObject);
var
  Start, Stop, Frequency: Int64;
  Filename: String;
  i: Integer;
  Max: Integer;
begin
  Filename := edFilename.Text;

  try
    Max := StrToInt( edLoops.Text );
  except
    Max := 1000;
    edLoops.Text := IntToStr( Max );
  end;

  Memo1.Clear;

(*
  if FileExists( Filename ) then
    Memo1.Lines.Add( 'File located/cached, begin testing.' )
    else
    begin
      Memo1.Lines.Add( 'File not found. Test canceled.' );
      Exit;
    end;
*)

  Memo1.Lines.Add( 'Begin Test #1: FileExists (' + IntToStr( Max ) + ' repeats)' );
  QueryPerformanceFrequency(Frequency);
  QueryPerformanceCounter(Start);
  for i := 0 to Max do
    if TestFileExists( Filename ) then
    else
    begin
      Memo1.Lines.Add( 'File not found. Test canceled.' );
      Break;
    end;
  QueryPerformanceCounter(Stop);
  Memo1.Lines.Add( 'Results for ' + IntToStr( Max ) + ' repeats: ' + FormatFloat('0.00', ( Stop - Start ) * 1000 / Frequency) + ' Milliseconds' );

  Memo1.Lines.Add( 'Begin Test #2: PIDL (' + IntToStr( Max ) + ' repeats)' );
  QueryPerformanceFrequency(Frequency);
  QueryPerformanceCounter(Start);
  for i := 0 to Max do
    if ( TestPIDL( Filename ) <> nil ) then
    else
    begin
      Memo1.Lines.Add( 'File not found. Test canceled.' );
      Break;
    end;
  QueryPerformanceCounter(Stop);
  Memo1.Lines.Add( 'Results for ' + IntToStr( Max ) + ' repeats: ' + FormatFloat('0.00', ( Stop - Start ) * 1000 / Frequency) + ' Milliseconds' );

  Memo1.Lines.Add( 'Begin Test #3: CreateFile (' + IntToStr( Max ) + ' repeats)' );
  QueryPerformanceFrequency(Frequency);
  QueryPerformanceCounter(Start);
  for i := 0 to Max do
    if TestCreateFile( Filename ) then
    else
    begin
      Memo1.Lines.Add( 'File not found. Test canceled.' );
      Break;
    end;
  QueryPerformanceCounter(Stop);
  Memo1.Lines.Add( 'Results for ' + IntToStr( Max ) + ' repeats: ' + FormatFloat('0.00', ( Stop - Start ) * 1000 / Frequency) + ' Milliseconds' );

  Memo1.Lines.Add( 'Begin Test #4: GetFileAtrributes (' + IntToStr( Max ) + ' repeats)' );
  QueryPerformanceFrequency(Frequency);
  QueryPerformanceCounter(Start);
  for i := 0 to Max do
    if TestGetFileAtrributes( Filename ) then
    else
    begin
      Memo1.Lines.Add( 'File not found. Test canceled.' );
      Break;
    end;
  QueryPerformanceCounter(Stop);
  Memo1.Lines.Add( 'Results for ' + IntToStr( Max ) + ' repeats: ' + FormatFloat('0.00', ( Stop - Start ) * 1000 / Frequency) + ' Milliseconds' );

  Memo1.Lines.Add( 'Begin Test #5: FileAge (' + IntToStr( Max ) + ' repeats)' );
  QueryPerformanceFrequency(Frequency);
  QueryPerformanceCounter(Start);
  for i := 0 to Max do
    if TestFileAge( Filename ) then
    else
    begin
      Memo1.Lines.Add( 'File not found. Test canceled.' );
      Break;
    end;
  QueryPerformanceCounter(Stop);
  Memo1.Lines.Add( 'Results for ' + IntToStr( Max ) + ' repeats: ' + FormatFloat('0.00', ( Stop - Start ) * 1000 / Frequency) + ' Milliseconds' );

  Memo1.Lines.Add( 'Begin Test #6: FileGetAttr (' + IntToStr( Max ) + ' repeats)' );
  QueryPerformanceFrequency(Frequency);
  QueryPerformanceCounter(Start);
  for i := 0 to Max do
    if TestFileGetAttr( Filename ) then
    else
    begin
      Memo1.Lines.Add( 'File not found. Test canceled.' );
      Break;
    end;
  QueryPerformanceCounter(Stop);
  Memo1.Lines.Add( 'Results for ' + IntToStr( Max ) + ' repeats: ' + FormatFloat('0.00', ( Stop - Start ) * 1000 / Frequency) + ' Milliseconds' );

  Memo1.Lines.Add( 'Begin Test #7: TFile (' + IntToStr( Max ) + ' repeats)' );
  QueryPerformanceFrequency(Frequency);
  QueryPerformanceCounter(Start);
  for i := 0 to Max do
    if TestTFile( Filename ) then
    else
    begin
      Memo1.Lines.Add( 'File not found. Test canceled.' );
      Break;
    end;
  QueryPerformanceCounter(Stop);
  Memo1.Lines.Add( 'Results for ' + IntToStr( Max ) + ' repeats: ' + FormatFloat('0.00', ( Stop - Start ) * 1000 / Frequency) + ' Milliseconds' );

  Memo1.Lines.Add( 'Begin Test #8: FindFirst (' + IntToStr( Max ) + ' repeats)' );
  QueryPerformanceFrequency(Frequency);
  QueryPerformanceCounter(Start);
  for i := 0 to Max do
    if TestFindFirst( Filename ) then
    else
    begin
      Memo1.Lines.Add( 'File not found. Test canceled.' );
      Break;
    end;
  QueryPerformanceCounter(Stop);
  Memo1.Lines.Add( 'Results for ' + IntToStr( Max ) + ' repeats: ' + FormatFloat('0.00', ( Stop - Start ) * 1000 / Frequency) + ' Milliseconds' );

  Memo1.Lines.Add( '' );
  Memo1.Lines.Add( 'Job Done.' );
end;

I build a small quick bencher, to me GetFileAttributes() is the winner and PItemIdList is by far biggest looser (if i implemented correct way.....)

FindFile.7z

  • Like 1
  • Thanks 1

Share this post


Link to post
2 hours ago, KodeZwerg said:

Please remove my crappy PIDL try, it cause MemoryLeak.

In the first 24h you can edit the post and remove the text yourself, but you cannot remove the post.

You should use the "Report Post" function for that, so one of the admins gets notified who can do that.

 

Share this post


Link to post
17 hours ago, KodeZwerg said:

open/closeuses
 ActiveX, Shlobj, IOUtils;

function TestFileExists( Filename: String ): Boolean;
begin
  Result := FileExists( Filename );
end;

type
  TParseDisplayName = function(pszPath: PWideChar; pbc: IBindCtx; var pidl: PItemIDList; sfgaoIn: ULong; var psfgaoOut: ULong): HResult; stdcall;

var
  SHParseDisplayName: TParseDisplayName;
  SHELL32DLLHandle : THandle;

function TestPIDL( Filename: String ): PItemIdList;
var
  PIDL: PItemIdList;
  Attrs: DWORD;
begin
  Result := nil;
  try
    CoInitialize(nil);
    if ( SHParseDisplayName( PChar( Filename ), nil, PIDL, 0, Attrs ) = S_OK ) then
      if Assigned( PIDL ) then
        Result := PIDL;
  finally
    CoUnInitialize();
  end;
end;

function TestCreateFile( Filename: String ): Boolean;
var
  hFile : THandle;
begin
  Result := False;
  hFile := CreateFile(PChar( Filename ), GENERIC_READ, FILE_SHARE_READ, nil, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
  if hFile <> INVALID_HANDLE_VALUE then
    begin
      Result := True;
      CloseHandle(hFile);
    end;
end;

function TestGetFileAtrributes( Filename: String ): Boolean;
var
  i: Cardinal;
begin
  Result := False;
  i := GetFileAttributes( PChar( Filename ) );
  if i <> INVALID_FILE_ATTRIBUTES then
    begin
      Result := True;
    end;
end;

function TestFileAge( Filename: String ): Boolean;
var
 DT: TDateTime;
begin
  Result := False;
  if FileAge( Filename, DT ) then
    begin
      Result := True;
    end;
end;

function TestFileGetAttr( Filename: String ): Boolean;
begin
  Result := False;
  if FileGetAttr( Filename ) <> 0 then
    begin
      Result := True;
    end;
end;

function TestTFile( Filename: String ): Boolean;
begin
  Result := False;
  if TFile.Exists( Filename ) then
    begin
      Result := True;
    end;
end;

function TestFindFirst( Filename: String ): Boolean;
var
  sr: TSearchRec;
begin
  Result := False;
  if FindFirst( Filename, faAnyFile, sr ) = 0 then
    begin
      Result := True;
    end;
  FindClose(sr);
end;


procedure TForm1.btnDoJobClick(Sender: TObject);
var
  Start, Stop, Frequency: Int64;
  Filename: String;
  i: Integer;
  Max: Integer;
begin
  Filename := edFilename.Text;

  try
    Max := StrToInt( edLoops.Text );
  except
    Max := 1000;
    edLoops.Text := IntToStr( Max );
  end;

  Memo1.Clear;

(*
  if FileExists( Filename ) then
    Memo1.Lines.Add( 'File located/cached, begin testing.' )
    else
    begin
      Memo1.Lines.Add( 'File not found. Test canceled.' );
      Exit;
    end;
*)

  Memo1.Lines.Add( 'Begin Test #1: FileExists (' + IntToStr( Max ) + ' repeats)' );
  QueryPerformanceFrequency(Frequency);
  QueryPerformanceCounter(Start);
  for i := 0 to Max do
    if TestFileExists( Filename ) then
    else
    begin
      Memo1.Lines.Add( 'File not found. Test canceled.' );
      Break;
    end;
  QueryPerformanceCounter(Stop);
  Memo1.Lines.Add( 'Results for ' + IntToStr( Max ) + ' repeats: ' + FormatFloat('0.00', ( Stop - Start ) * 1000 / Frequency) + ' Milliseconds' );

  Memo1.Lines.Add( 'Begin Test #2: PIDL (' + IntToStr( Max ) + ' repeats)' );
  QueryPerformanceFrequency(Frequency);
  QueryPerformanceCounter(Start);
  for i := 0 to Max do
    if ( TestPIDL( Filename ) <> nil ) then
    else
    begin
      Memo1.Lines.Add( 'File not found. Test canceled.' );
      Break;
    end;
  QueryPerformanceCounter(Stop);
  Memo1.Lines.Add( 'Results for ' + IntToStr( Max ) + ' repeats: ' + FormatFloat('0.00', ( Stop - Start ) * 1000 / Frequency) + ' Milliseconds' );

  Memo1.Lines.Add( 'Begin Test #3: CreateFile (' + IntToStr( Max ) + ' repeats)' );
  QueryPerformanceFrequency(Frequency);
  QueryPerformanceCounter(Start);
  for i := 0 to Max do
    if TestCreateFile( Filename ) then
    else
    begin
      Memo1.Lines.Add( 'File not found. Test canceled.' );
      Break;
    end;
  QueryPerformanceCounter(Stop);
  Memo1.Lines.Add( 'Results for ' + IntToStr( Max ) + ' repeats: ' + FormatFloat('0.00', ( Stop - Start ) * 1000 / Frequency) + ' Milliseconds' );

  Memo1.Lines.Add( 'Begin Test #4: GetFileAtrributes (' + IntToStr( Max ) + ' repeats)' );
  QueryPerformanceFrequency(Frequency);
  QueryPerformanceCounter(Start);
  for i := 0 to Max do
    if TestGetFileAtrributes( Filename ) then
    else
    begin
      Memo1.Lines.Add( 'File not found. Test canceled.' );
      Break;
    end;
  QueryPerformanceCounter(Stop);
  Memo1.Lines.Add( 'Results for ' + IntToStr( Max ) + ' repeats: ' + FormatFloat('0.00', ( Stop - Start ) * 1000 / Frequency) + ' Milliseconds' );

  Memo1.Lines.Add( 'Begin Test #5: FileAge (' + IntToStr( Max ) + ' repeats)' );
  QueryPerformanceFrequency(Frequency);
  QueryPerformanceCounter(Start);
  for i := 0 to Max do
    if TestFileAge( Filename ) then
    else
    begin
      Memo1.Lines.Add( 'File not found. Test canceled.' );
      Break;
    end;
  QueryPerformanceCounter(Stop);
  Memo1.Lines.Add( 'Results for ' + IntToStr( Max ) + ' repeats: ' + FormatFloat('0.00', ( Stop - Start ) * 1000 / Frequency) + ' Milliseconds' );

  Memo1.Lines.Add( 'Begin Test #6: FileGetAttr (' + IntToStr( Max ) + ' repeats)' );
  QueryPerformanceFrequency(Frequency);
  QueryPerformanceCounter(Start);
  for i := 0 to Max do
    if TestFileGetAttr( Filename ) then
    else
    begin
      Memo1.Lines.Add( 'File not found. Test canceled.' );
      Break;
    end;
  QueryPerformanceCounter(Stop);
  Memo1.Lines.Add( 'Results for ' + IntToStr( Max ) + ' repeats: ' + FormatFloat('0.00', ( Stop - Start ) * 1000 / Frequency) + ' Milliseconds' );

  Memo1.Lines.Add( 'Begin Test #7: TFile (' + IntToStr( Max ) + ' repeats)' );
  QueryPerformanceFrequency(Frequency);
  QueryPerformanceCounter(Start);
  for i := 0 to Max do
    if TestTFile( Filename ) then
    else
    begin
      Memo1.Lines.Add( 'File not found. Test canceled.' );
      Break;
    end;
  QueryPerformanceCounter(Stop);
  Memo1.Lines.Add( 'Results for ' + IntToStr( Max ) + ' repeats: ' + FormatFloat('0.00', ( Stop - Start ) * 1000 / Frequency) + ' Milliseconds' );

  Memo1.Lines.Add( 'Begin Test #8: FindFirst (' + IntToStr( Max ) + ' repeats)' );
  QueryPerformanceFrequency(Frequency);
  QueryPerformanceCounter(Start);
  for i := 0 to Max do
    if TestFindFirst( Filename ) then
    else
    begin
      Memo1.Lines.Add( 'File not found. Test canceled.' );
      Break;
    end;
  QueryPerformanceCounter(Stop);
  Memo1.Lines.Add( 'Results for ' + IntToStr( Max ) + ' repeats: ' + FormatFloat('0.00', ( Stop - Start ) * 1000 / Frequency) + ' Milliseconds' );

  Memo1.Lines.Add( '' );
  Memo1.Lines.Add( 'Job Done.' );
end;

I build a small quick bencher, to me GetFileAttributes() is the winner and PItemIdList is by far biggest looser (if i implemented correct way.....)

FindFile.7z

Please exclude TestPIDL method out of DoJob method, it produce MemoryLeak and is slowest methode anyway.

Share this post


Link to post

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

×