LOGO OA教程 ERP教程 模切知识交流 PMS教程 CRM教程 开发文档 其他文档  
 
网站管理员

为什么说方法的参数最好不要超过4个?

freeflydom
2025年7月2日 8:21 本文热度 45

简介

在很多年前的一次Code Review中,有大佬指出,方法的参数太多了,最好不要超过四个,对于当时还是萌新的我,虽然不知道什么原因,但听人劝,吃饱饭,这个习惯也就传递下来了,直到参加工作很多年后,才明白这其中的缘由。

调用协定

在计算机编程中,调用协定(Calling Convention)是一套关于方法/函数被调用时参数传递方式栈由谁清理寄存器如何使用的规范。

  1. 参数传递方式
  • 寄存器传递:将参数存入CPU寄存器,速度最快。
  • 栈传递:将参数压入调用栈,再依次从栈中取出,速度最慢
  • 混合传递:前N个参数用寄存器,剩余参数用栈,速度适中
  1. 栈由谁清理
  • Caller清理:调用函数后由调用方负责恢复栈指针(如C/C++的__cdecl)。
  • Callee清理:被调用函数返回前自行清理栈(如x64的默认协定)。
  1. 寄存器如何使用
  • 易变寄存器(Volatile Registers):函数调用时可能被修改的寄存器(如x64的RAXRCXRDX),调用方需自行保存这些寄存器的值。
  • 非易变寄存器(Non-Volatile Registers):函数必须保存并恢复的寄存器(如x64的RBXRBPR12-R15)。

x86架构混乱的调用协定

x86架构发展较早,因此调用协定野蛮生长,有多种调用协定

协定名称参数传递方式栈清理适用场景
__cdecl通过栈传递(右→左)调用者清理栈C/C++默认,支持可变参数
__stdcall通过栈传递(右→左)被调用者清理栈Windows API(如Win32)
__fastcall前两个参数通过寄存器,剩余通过栈(右→左)被调用者清理栈高性能场景
__thiscallthis指针通过寄存器, 剩余通过栈(右→左)被调用者清理栈C++类成员函数

眼见为实

可以看到,cdecl,stdcall是通过压栈的方式将参数压入栈中,而fastcall直接赋值给寄存器,并无压栈操作

点击查看代码
#include <iostream>
int __cdecl cdecl_add(int a, int b) {
	return a + b;
}
int __stdcall stdcall_add(int a, int b) {
	return a + b;
}
int __fastcall fastcall_add(int a, int b) {
	return a + b;
}
class Calculator {
public:
	int __thiscall thiscall_add(int b) {
		return this->a + b;
	}
	int a;
};
int main()
{
	int a = 10, b = 5;
	int cdecl_add_value = cdecl_add(a, b);
	int stdcall_add_value = stdcall_add(a, b);
	int fastcall_add_value = fastcall_add(a, b);
	Calculator calc;
	calc.a = 10;
	int thiscall_add_value = calc.thiscall_add(5);
}

x64的大一统

而在x64架构下,为了解决割裂的调用协定,windows与linux实现了统一。

协定名称参数传递方式栈清理适用场景
MS x64前4个参数通过寄存器,剩余通过栈(左→右)被调用者清理栈Windows x64程序
System V AMD64前6个参数通过寄存器,剩余通过栈(左→右)被调用者清理栈Unix/Linux x64程序

眼见为实

linux下暂无图(因为我懒),大概就是这意思,自行脑补

点击查看代码
#include <stdio.h>
int add(int a, int b, int c, int d, int e) {
    return a + b + c + d + e;
}
int main() {
    int result = add(1, 2, 3, 4, 5);
    return 0;
}

C#中使用哪种调用协定?

C#在x86下,有自己独特的调用协定

协定名称参数传递方式栈清理适用场景
Standard前两个参数通过寄存器,剩余通过栈(左→右)被调用者清理栈C#静态方法
HasThis前两个参数通过寄存器(第一个为This),剩余通过栈(左→右)被调用者清理栈C#实例方法

在x64形成实现统一,与操作系统保持一致

眼见为实



注意寄存器与栈是两片独立运行的区域,光从汇编代码,很容易陷入误区,就拿上图来说,从上往下阅读汇编,你会发现参数传递的顺序是30(1Eh),40(28h),50(32h),10(0Ah),20(14h)。明显不对,这是因为一个是寄存器,一个是线程栈,这是两个不相关的区域,谁前谁后都不违反从左到右的规定。不能死脑筋,寄存器与栈之间是存在位置无关性的。

/*这种顺序也是正确的,寄存器是寄存器,栈是栈,汇编的顺序不影响他们的位置无关性,因为是两片独立运行的区域*/
push 1Eh
mov ecx,0Ah
push 28h
mov edx,14h
push 32h
点击查看代码
    internal class Program
    {
        static void Main(string[] args)
        {
            var t = new Test();
            var sum = t.Add(10, 20, 30, 40, 50);
            var sum2 = Test.StaticAdd(10, 20, 30, 40, 50);
            Console.ReadKey();
        }
    }
    public class Test
    {
        public int Add(int a, int b, int c, int d, int e)
        {
            var sum = a + b + c + d + e;
            return sum;
        }
        public static int StaticAdd(int a, int b, int c, int d, int e)
        {
            var sum = a + b + c + d + e;
            return sum;
        }
    }

结论

可以看到,在Windows x64下,如果方法的参数<=4 那么就就完全避免了栈传递的开销,实现性能最佳化。

在linux下,参数为<=6,根据木桶效应,取4为最佳。

当然,此文不是让你严格遵守此规则,随着CPU性能的发展,在微服务集群大行其道的今天。这点性能差距可以忽略不计,权当饭后消遣,补充冷知识,好让你在未来的Code Review中,没活硬整.

点击查看代码
    internal class Program
    {
        static void Main(string[] args)
        {
            ParameterPassingBenchmark.Run();
        }
    }
    public class ParameterPassingBenchmark
    {
        private const int WarmupIterations = 100000;
        private const int BenchmarkIterations = 10000000;
        private const int BatchSize = 1000; // 批量调用次数,提高测量精度
        private static readonly Random _random = new Random(42);
        // x64平台前4个参数通过寄存器传递
        [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)]
        public static int Register4Params(int a, int b, int c, int d) => a + b + c + d;
        // 第5个参数通过栈传递
        [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)]
        public static int Stack1Param(int a, int b, int c, int d, int e) => a + b + c + d + e;
        // 第5-8个参数通过栈传递
        [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)]
        public static int Stack4Params(int a, int b, int c, int d, int e, int f, int g, int h)
            => a + b + c + d + e + f + g + h;
        public static void Run()
        {
            Console.WriteLine($"参数传递性能测试 - 预热: {WarmupIterations:N0}, 测试: {BenchmarkIterations:N0} 次");
            Console.WriteLine("----------------------------------------------------------------");
            // 生成随机输入数据以避免优化
            var inputData = GenerateInputData();
            // 预热
            Warmup(inputData);
            // 测试
            var reg4Time = Measure(() => Register4ParamsTest(inputData));
            var stack1Time = Measure(() => Stack1ParamTest(inputData));
            var stack4Time = Measure(() => Stack4ParamsTest(inputData));
            // 输出结果
            Console.WriteLine("\n===== 测试结果 =====");
            Console.WriteLine($"4寄存器参数: {reg4Time,12:N2} ns/次");
            Console.WriteLine($"4寄存器+1栈参数: {stack1Time,10:N2} ns/次 ({((double)stack1Time / reg4Time - 1) * 100:F1}% 性能下降)");
            Console.WriteLine($"4寄存器+4栈参数: {stack4Time,10:N2} ns/次 ({((double)stack4Time / reg4Time - 1) * 100:F1}% 性能下降)");
        }
        private static (int[], int[], int[]) GenerateInputData()
        {
            var data4 = new int[BenchmarkIterations * 4];
            var data5 = new int[BenchmarkIterations * 5];
            var data8 = new int[BenchmarkIterations * 8];
            for (int i = 0; i < BenchmarkIterations; i++)
            {
                for (int j = 0; j < 4; j++) data4[i * 4 + j] = _random.Next();
                for (int j = 0; j < 5; j++) data5[i * 5 + j] = _random.Next();
                for (int j = 0; j < 8; j++) data8[i * 8 + j] = _random.Next();
            }
            return (data4, data5, data8);
        }
        private static void Warmup((int[], int[], int[]) inputData)
        {
            Console.Write("预热中...");
            var (data4, data5, data8) = inputData;
            for (int i = 0; i < WarmupIterations; i++)
            {
                Register4Params(data4[i * 4], data4[i * 4 + 1], data4[i * 4 + 2], data4[i * 4 + 3]);
                Stack1Param(data5[i * 5], data5[i * 5 + 1], data5[i * 5 + 2], data5[i * 5 + 3], data5[i * 5 + 4]);
                Stack4Params(data8[i * 8], data8[i * 8 + 1], data8[i * 8 + 2], data8[i * 8 + 3],
                            data8[i * 8 + 4], data8[i * 8 + 5], data8[i * 8 + 6], data8[i * 8 + 7]);
            }
            Console.WriteLine("完成");
        }
        private static long Measure(Func<long> testMethod)
        {
            // 强制GC并等待完成
            GC.Collect();
            GC.WaitForPendingFinalizers();
            GC.Collect();
            // 冷启动
            testMethod();
            // 实际测量
            var stopwatch = Stopwatch.StartNew();
            long result = testMethod();
            stopwatch.Stop();
            // 使用结果以避免被优化掉
            if (result == 0) Console.WriteLine("警告: 结果为0,可能存在优化问题");
            // 计算平均时间(纳秒)
            long totalNs = stopwatch.ElapsedTicks * 10000000L / Stopwatch.Frequency;
            return totalNs / (BenchmarkIterations / BatchSize); // 除以实际调用批次
        }
        private static long Register4ParamsTest((int[], int[], int[]) inputData)
        {
            var (data4, _, _) = inputData;
            long sum = 0;
            int index = 0;
            for (int i = 0; i < BenchmarkIterations / BatchSize; i++)
            {
                // 批量调用以提高测量精度
                for (int j = 0; j < BatchSize; j++)
                {
                    sum += Register4Params(
                        data4[index++],
                        data4[index++],
                        data4[index++],
                        data4[index++]
                    );
                }
            }
            return sum;
        }
        private static long Stack1ParamTest((int[], int[], int[]) inputData)
        {
            var (_, data5, _) = inputData;
            long sum = 0;
            int index = 0;
            for (int i = 0; i < BenchmarkIterations / BatchSize; i++)
            {
                // 批量调用以提高测量精度
                for (int j = 0; j < BatchSize; j++)
                {
                    sum += Stack1Param(
                        data5[index++],
                        data5[index++],
                        data5[index++],
                        data5[index++],
                        data5[index++]
                    );
                }
            }
            return sum;
        }
        private static long Stack4ParamsTest((int[], int[], int[]) inputData)
        {
            var (_, _, data8) = inputData;
            long sum = 0;
            int index = 0;
            for (int i = 0; i < BenchmarkIterations / BatchSize; i++)
            {
                // 批量调用以提高测量精度
                for (int j = 0; j < BatchSize; j++)
                {
                    sum += Stack4Params(
                        data8[index++],
                        data8[index++],
                        data8[index++],
                        data8[index++],
                        data8[index++],
                        data8[index++],
                        data8[index++],
                        data8[index++]
                    );
                }
            }
            return sum;
        }
    }

转自https://www.cnblogs.com/lmy5215006/p/18919081


该文章在 2025/7/2 8:21:37 编辑过
关键字查询
相关文章
正在查询...
点晴ERP是一款针对中小制造业的专业生产管理软件系统,系统成熟度和易用性得到了国内大量中小企业的青睐。
点晴PMS码头管理系统主要针对港口码头集装箱与散货日常运作、调度、堆场、车队、财务费用、相关报表等业务管理,结合码头的业务特点,围绕调度、堆场作业而开发的。集技术的先进性、管理的有效性于一体,是物流码头及其他港口类企业的高效ERP管理信息系统。
点晴WMS仓储管理系统提供了货物产品管理,销售管理,采购管理,仓储管理,仓库管理,保质期管理,货位管理,库位管理,生产管理,WMS管理系统,标签打印,条形码,二维码管理,批号管理软件。
点晴免费OA是一款软件和通用服务都免费,不限功能、不限时间、不限用户的免费OA协同办公管理系统。
Copyright 2010-2025 ClickSun All Rights Reserved