Wednesday, July 8, 2020

C# .NET IO.Path.GetExtension vs PInvoke Win32 PathFindExtension Native API Speed Comparison

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