Here's how to get a file extension comparing P/Invoke Win32 API Native PathFindExtension with .NET IO.Path.GetExtension method.
Analysis
P/Invoke PathRemoveExtension on 1st run is 'slow' due to overhead and loading metadata into cache. On successive runs, this drops down to 4 ticks.
Likewise, Path.GetExtension on initial load is 'slow' to load metadata in cache. On successive runs this is 2 ticks.
Using PrepareMethod to pre-cache a method call but the call costed to much, therefore for preformance gain, just call the method twice.
Run results
Built-in Path.IO Get File Extension - 1st run exe in ticks 14 Built-in Path.IO Get File Extension - 2nd run exe in ticks 2 P/Invoke PathFindExtension - DOES NOT WORK? C:\Windows\write.exe in ticks 1554 PrepareMethod Path.IO GetFileExtension Encapsulated Method - 1st run exe in ticks 3969 Call Path.IO GetFileExtension Encapsulated Method - 2nd run exe in ticks 4 P/Invoke PathRemoveExtension - 1st run exe in ticks 131 P/Invoke PathRemoveExtension - 2nd run exe in ticks 4 P/Invoke PathRemoveExtension - 3rd run exe in ticks 4
Source Code (can't use online .NET Fiddle for P/Invoke calls)
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.InteropServices;
using System.Diagnostics;
using System.IO;
using System.Runtime.CompilerServices;
using System.Reflection;
namespace GetFileExtensionPInvoke
{
class Program
{
public static class MethodWarmerUper
{
public static void WarmUp(string methodName)
{
var handle = FindMethodWithName(methodName).MethodHandle;
RuntimeHelpers.PrepareMethod(handle);
}
private static MethodInfo FindMethodWithName(string methodName)
{
return
Assembly.GetExecutingAssembly()
.GetTypes()
.SelectMany(type => type.GetMethods(MethodBindingFlags))
.FirstOrDefault(method => method.Name == methodName);
}
private const BindingFlags MethodBindingFlags =
BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.NonPublic |
BindingFlags.Instance | BindingFlags.Static;
}
public static class IOMethod
{
public static string GetFileExtension(string s)
{
return Path.GetExtension(s);
}
}
// <summary>
/// Removes the file name extension from a path, if one is present.
/// </summary>
/// <param name="pszFile">A pointer to a null-terminated string of length MAX_PATH from which to remove the extension.</param>
[DllImport("shlwapi.dll", EntryPoint = "PathRemoveExtensionW", SetLastError = true, CharSet = CharSet.Unicode)]
public static extern void PathRemoveExtension([MarshalAs(UnmanagedType.LPWStr)]System.Text.StringBuilder pszFile);
//https://docs.microsoft.com/en-us/windows/win32/api/shlwapi/nf-shlwapi-pathfindextensionw
// <summary>
/// Returns the address of the "." that precedes the extension within pszPath if an extension is found, or the address of the terminating null character otherwise.
/// </summary>
/// <param name="pszFile">A pointer to a null-terminated string of length MAX_PATH from which to remove the extension.</param>
[DllImport("shlwapi.dll", EntryPoint = "PathFindExtensionW", SetLastError = true, CharSet = CharSet.Unicode)]
public static extern void PathFindExtension([MarshalAs(UnmanagedType.LPWStr)]System.Text.StringBuilder pszFile);
static void Main(string[] args)
{
string filepath = @"C:\Windows\write.exe";
string ext = string.Empty;
StringBuilder sbExt = new StringBuilder(filepath);
Stopwatch sw = new Stopwatch();
Console.WriteLine("\nBuilt-in Path.IO Get File Extension - 1st run");
sw.Start();
ext = Path.GetExtension(filepath);
sw.Stop();
ext = ext.TrimStart('.'); //add 1 tick
Console.WriteLine(ext + " in ticks " + sw.ElapsedTicks);
sw.Reset();
Console.WriteLine("Built-in Path.IO Get File Extension - 2nd run");
sw.Start();
ext = Path.GetExtension(filepath);
sw.Stop();
ext = ext.TrimStart('.'); //add 1 tick
Console.WriteLine(ext + " in ticks " + sw.ElapsedTicks);
sw.Reset();
Console.WriteLine("\nP/Invoke PathFindExtension - DOES NOT WORK?");
sw.Start();
PathFindExtension(sbExt);
//Console.Error.WriteLine("Get Win32 Error = "+ Marshal.GetLastWin32Error());
sw.Stop();
Console.WriteLine(sbExt.ToString() + " in ticks " + sw.ElapsedTicks);
Console.WriteLine("");
sw.Reset();
Console.WriteLine("PrepareMethod Path.IO GetFileExtension Encapsulated Method - 1st run");
sw.Start();
MethodWarmerUper.WarmUp("GetFileExtension"); //very poor performance
sw.Stop();
ext = ext.TrimStart('.'); //add 1 tick
Console.WriteLine(ext + " in ticks " + sw.ElapsedTicks);
sw.Reset();
Console.WriteLine("Call Path.IO GetFileExtension Encapsulated Method - 2nd run");
sw.Start();
ext = IOMethod.GetFileExtension(filepath);
sw.Stop();
ext = ext.TrimStart('.'); //add 1 tick
Console.WriteLine(ext + " in ticks " + sw.ElapsedTicks);
sw.Reset();
Console.WriteLine("");
sbExt = new StringBuilder(filepath);
Console.WriteLine("P/Invoke PathRemoveExtension - 1st run");
sw.Start();
PathRemoveExtension(sbExt);
ext = sbExt.ToString();
ext = filepath.Replace(ext,"");
ext = ext.TrimStart('.'); //add 1 tick
sw.Stop();
Console.WriteLine(ext + " in ticks " + sw.ElapsedTicks);
sw.Reset();
sbExt = new StringBuilder(filepath);
Console.WriteLine("P/Invoke PathRemoveExtension - 2nd run");
sw.Start();
PathRemoveExtension(sbExt);
ext = sbExt.ToString();
ext = filepath.Replace(ext, "");
ext = ext.TrimStart('.'); //add 1 tick
sw.Stop();
Console.WriteLine(ext + " in ticks " + sw.ElapsedTicks);
sw.Reset();
sbExt = new StringBuilder(filepath);
Console.WriteLine("P/Invoke PathRemoveExtension - 3rd run");
sw.Start();
PathRemoveExtension(sbExt);
ext = sbExt.ToString();
ext = filepath.Replace(ext, "");
ext = ext.TrimStart('.'); //add 1 tick
sw.Stop();
Console.WriteLine(ext + " in ticks " + sw.ElapsedTicks);
sw.Reset();
Console.ReadKey();
}
}
}

No comments:
Post a Comment