当前位置: 首页 > news >正文

.NET 中 GetProcess 相关方法的性能

.NET 的 Process 类中提供了查找进程的若干方法,其中部分方法还比较消耗性能。如果你试图优化查找进程相关方法的性能,可能本文分享的一些耗时数据可以作为参考。


性能比较

Process 类中提供了四种查询进程的方法:

  • GetProcesses
    • 获取当前计算机或远程计算机上运行的所有进程。
  • GetProcessById
    • 获取当前计算机或远程计算机上 pid 为 指定值的进程。
  • GetProcessesByName
    • 根据进程的名字查找当前计算机或远程计算机上的进程。
  • GetCurrentProcess
    • 获取当前进程的 Process 实例。

先给出我的实测数据(100 次执行耗时):

  • Process.GetProcesses()
    • 00:00:00.7254688
  • Process.GetProcessById(id)
    • 00:00:01.3660640(实际数值取决于当前进程数)
  • Process.GetProcessesByName("Walterlv.Demo")
    • 00:00:00.5604279
  • Process.GetCurrentProcess()
    • 00:00:00.0000546

结果显示获取所有进程实例的 GetProcesses 方法速度竟然比获取单个进程实例的 GetProcessById 还要快得多!额外地,根据名称查找进程比前两者都快,获取当前进程实例的方法快得不是一个数量级。

这些速度差异源于哪里

我们先看看最慢的方法 GetProcessIds,它的最本质的实现在 ProcessManager 类中:

// ProcessManager
public static int[] GetProcessIds() {
    int[] processIds = new int[256];
    int size;
    for (;;) {
        if (!NativeMethods.EnumProcesses(processIds, processIds.Length * 4, out size))
            throw new Win32Exception();
        if (size == processIds.Length * 4) {
            processIds = new int[processIds.Length * 2];
            continue;
        }
        break;
    }
    int[] ids = new int[size / 4];
    Array.Copy(processIds, ids, ids.Length);
    return ids;
}

先创建一个 256 长度的数组,然后使用本机函数枚举进程列表填充这个数组。如果实际所需的数组大小与传入的数组大小相等,说明数组用完了,有可能进程数比 256 个多。所以,将数组长度扩大为两倍,随后再试一次。直到发现申请的数组长度足够存下进程数为止。

这里用到了本机方法 EnumProcesses 来枚举进程。传入的 size 要乘以 4 是因为传入的是字节数,一个 int 是 4 个字节。

// NativeMethods
[DllImport("psapi.dll", CharSet=System.Runtime.InteropServices.CharSet.Auto, SetLastError=true)]
public static extern bool EnumProcesses(int[] processIds, int size, out int needed);

所以我们可以得知,如果当前计算机中的进程数小于 256 个,那么枚举进程方法仅需执行一次;而如果大于或等于 256 个,则枚举进程的方法需要执行两次或更多次,这是性能很差的一个重要原因。

另外,GetProcesses 方法就要复杂得多,其核心调用的是 ProcessManager.GetProcessInfos 方法。方法很长,但其大体思路是获取当前计算机上的线程列表,然后将线程所在的进程储存到哈希表中(相当于去重),随后返回此哈希表的数组副本。

// ProcessManager
static ProcessInfo[] GetProcessInfos(IntPtr dataPtr, Predicate<int> processIdFilter) {
    // 60 is a reasonable number for processes on a normal machine.
    Hashtable processInfos = new Hashtable(60);

    long totalOffset = 0;

    while(true) {
        IntPtr currentPtr = (IntPtr)((long)dataPtr + totalOffset);
        SystemProcessInformation pi = new SystemProcessInformation();

        Marshal.PtrToStructure(currentPtr, pi);

        // Process ID shouldn't overflow. OS API GetCurrentProcessID returns DWORD.
        int processInfoProcessId = pi.UniqueProcessId.ToInt32();

        if (processIdFilter == null || processIdFilter(processInfoProcessId)) {
            // get information for a process
            ProcessInfo processInfo = new ProcessInfo();
            processInfo.processId = processInfoProcessId;
            processInfo.handleCount = (int)pi.HandleCount;
            processInfo.sessionId = (int)pi.SessionId;                
            processInfo.poolPagedBytes = (long)pi.QuotaPagedPoolUsage;;
            processInfo.poolNonpagedBytes = (long)pi.QuotaNonPagedPoolUsage;
            processInfo.virtualBytes = (long)pi.VirtualSize;
            processInfo.virtualBytesPeak = (long)pi.PeakVirtualSize;
            processInfo.workingSetPeak = (long)pi.PeakWorkingSetSize;
            processInfo.workingSet = (long)pi.WorkingSetSize;
            processInfo.pageFileBytesPeak = (long)pi.PeakPagefileUsage;
            processInfo.pageFileBytes = (long)pi.PagefileUsage;
            processInfo.privateBytes = (long)pi.PrivatePageCount;
            processInfo.basePriority = pi.BasePriority;


            if( pi.NamePtr == IntPtr.Zero) {                    
                if( processInfo.processId == NtProcessManager.SystemProcessID) {
                    processInfo.processName = "System";
                }
                else if( processInfo.processId == NtProcessManager.IdleProcessID) {
                    processInfo.processName = "Idle";
                }
                else { 
                    // for normal process without name, using the process ID. 
                    processInfo.processName = processInfo.processId.ToString(CultureInfo.InvariantCulture);
                }
            }
            else {                     
                string processName = GetProcessShortName(Marshal.PtrToStringUni(pi.NamePtr, pi.NameLength/sizeof(char)));  
                //
                // On old operating system (NT4 and windows 2000), the process name might be truncated to 15 
                // characters. For example, aspnet_admin.exe will show up in performance counter as aspnet_admin.ex.
                // Process class try to return a nicer name. We used to get the main module name for a process and 
                // use that as the process name. However normal user doesn't have access to module information, 
                // so normal user will see an exception when we try to get a truncated process name.
                //                    
                if (ProcessManager.IsOSOlderThanXP && (processName.Length == 15)) {
                    if (processName.EndsWith(".", StringComparison.OrdinalIgnoreCase)) {
                        processName = processName.Substring(0, 14);
                    }
                    else if (processName.EndsWith(".e", StringComparison.OrdinalIgnoreCase)) {
                        processName = processName.Substring(0, 13);
                    }
                    else if (processName.EndsWith(".ex", StringComparison.OrdinalIgnoreCase)) {
                        processName = processName.Substring(0, 12);
                    }
                }
                processInfo.processName = processName;                                          
            }

            // get the threads for current process
            processInfos[processInfo.processId] =  processInfo;

            currentPtr = (IntPtr)((long)currentPtr + Marshal.SizeOf(pi));
            int i = 0;
            while( i < pi.NumberOfThreads) {
                SystemThreadInformation ti = new SystemThreadInformation();
                Marshal.PtrToStructure(currentPtr, ti);                    
                ThreadInfo threadInfo = new ThreadInfo();                    

                threadInfo.processId = (int)ti.UniqueProcess;
                threadInfo.threadId = (int)ti.UniqueThread;
                threadInfo.basePriority = ti.BasePriority;
                threadInfo.currentPriority = ti.Priority;
                threadInfo.startAddress = ti.StartAddress;
                threadInfo.threadState = (ThreadState)ti.ThreadState;
                threadInfo.threadWaitReason = NtProcessManager.GetThreadWaitReason((int)ti.WaitReason);

                processInfo.threadInfoList.Add(threadInfo);
                currentPtr = (IntPtr)((long)currentPtr + Marshal.SizeOf(ti));
                i++;
            }
        }

        if (pi.NextEntryOffset == 0) {
            break;
        }
        totalOffset += pi.NextEntryOffset;
    }

    ProcessInfo[] temp = new ProcessInfo[processInfos.Values.Count];
    processInfos.Values.CopyTo(temp, 0);
    return temp;
}

GetProcessesByName 方法就比较奇怪了,因为其本质上就是调用了 Process.GetProcesses 方法,并在其后额外执行了一些代码。理论上不应该出现耗时更短的情况。事实上,在测试中,我将 GetProcessesGetProcessesByName 方法的执行调换顺序也能得到稳定一致的结果,都是 GetProcessesByName 更快。

public static Process[] GetProcessesByName(string processName, string machineName) {
    if (processName == null) processName = String.Empty;
    Process[] procs = GetProcesses(machineName);
    ArrayList list = new ArrayList();

    for(int i = 0; i < procs.Length; i++) {                
        if( String.Equals(processName, procs[i].ProcessName, StringComparison.OrdinalIgnoreCase)) {
            list.Add( procs[i]);                    
        } else {
            procs[i].Dispose();
        }
    }

    Process[] temp = new Process[list.Count];
    list.CopyTo(temp, 0);
    return temp;
}

至于 GetCurrentProcess 方法能够这么快,很好理解,毕竟是自己进程,有什么拿不到的呢?其内部调用的是本机方法:

[DllImport("kernel32.dll", CharSet=System.Runtime.InteropServices.CharSet.Auto)]
public static extern int GetCurrentProcessId();

另外,有个有意思的现象:

  • Windows的PID为什么是4的倍数 - 开源中国社区
  • WINDOWS进程或线程号为什么是4的倍数 - GUO Xingwang - 博客园

相关文章:

  • 常用输入法快速输入自定义格式的时间和日期(搜狗/QQ/手心/微软拼音)
  • 好的框架需要好的 API 设计 —— API 设计的六个原则
  • .NET/C# 使用反射注册事件
  • .NET/C# 判断某个类是否是泛型类型或泛型接口的子类型
  • .NET/C# 使用反射调用含 ref 或 out 参数的方法
  • WPF 多线程 UI:设计一个异步加载 UI 的容器
  • .NET 命令行参数包含应用程序路径吗?
  • 分析现有 WPF / Windows Forms 程序能否顺利迁移到 .NET Core 3.0(使用 .NET Core 3.0 Desktop API Analyzer )
  • C# 空合并操作符(??)不可重载?其实有黑科技可以间接重载!
  • UWP 轻量级样式定义(Lightweight Styling)
  • 预编译框架,开发高性能应用 - 课程 - 微软技术暨生态大会 2018
  • 将 UWP 中 CommandBar 的展开方向改为向下展开
  • .NET 中创建支持集合初始化器的类型
  • .NET 中让 Task 支持带超时的异步等待
  • .NET/C# 中你可以在代码中写多个 Main 函数,然后按需要随时切换
  • [case10]使用RSQL实现端到端的动态查询
  • bootstrap创建登录注册页面
  • IndexedDB
  • Iterator 和 for...of 循环
  • JavaScript实现分页效果
  • 产品三维模型在线预览
  • 从零到一:用Phaser.js写意地开发小游戏(Chapter 3 - 加载游戏资源)
  • 对话:中国为什么有前途/ 写给中国的经济学
  • 搞机器学习要哪些技能
  • 机器人定位导航技术 激光SLAM与视觉SLAM谁更胜一筹?
  • 基于Mobx的多页面小程序的全局共享状态管理实践
  • 使用 @font-face
  • 思维导图—你不知道的JavaScript中卷
  • 用jquery写贪吃蛇
  • 不要一棍子打翻所有黑盒模型,其实可以让它们发挥作用 ...
  • 哈罗单车融资几十亿元,蚂蚁金服与春华资本加持 ...
  • 进程与线程(三)——进程/线程间通信
  • #stm32整理(一)flash读写
  • (Redis使用系列) SpirngBoot中关于Redis的值的各种方式的存储与取出 三
  • (动手学习深度学习)第13章 计算机视觉---图像增广与微调
  • (二)【Jmeter】专栏实战项目靶场drupal部署
  • (四)图像的%2线性拉伸
  • (转)jQuery 基础
  • (转)mysql使用Navicat 导出和导入数据库
  • (转载)OpenStack Hacker养成指南
  • (轉)JSON.stringify 语法实例讲解
  • .axf 转化 .bin文件 的方法
  • .NET 6 Mysql Canal (CDC 增量同步,捕获变更数据) 案例版
  • .NET Core WebAPI中封装Swagger配置
  • .net mvc 获取url中controller和action
  • .NET 依赖注入和配置系统
  • .Net 应用中使用dot trace进行性能诊断
  • .NET/C# 使窗口永不获得焦点
  • .Net程序帮助文档制作
  • .NET简谈互操作(五:基础知识之Dynamic平台调用)
  • @KafkaListener注解详解(一)| 常用参数详解
  • [ 第一章] JavaScript 简史
  • [C++ 从入门到精通] 12.重载运算符、赋值运算符重载、析构函数
  • [Dxperience.8.*]报表预览控件PrintControl设置
  • [hdu 2826] The troubles of lmy [简单计算几何 - 相似]