寇秀娟 发表于 2025-6-25 21:28:36

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

简介

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

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

[*]参数传递方式


[*]寄存器传递:将参数存入CPU寄存器,速度最快。
[*]栈传递:将参数压入调用栈,再依次从栈中取出,速度最慢
[*]混合传递:前N个参数用寄存器,剩余参数用栈,速度适中

[*]栈由谁清理


[*]Caller清理:调用函数后由调用方负责恢复栈指针(如C/C++的__cdecl)。
[*]Callee清理:被调用函数返回前自行清理栈(如x64的默认协定)。

[*]寄存器如何使用


[*]易变寄存器(Volatile Registers):函数调用时可能被修改的寄存器(如x64的RAX、RCX、RDX),调用方需自行保存这些寄存器的值。
[*]非易变寄存器(Non-Volatile Registers):函数必须保存并恢复的寄存器(如x64的RBX、RBP、R12-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形成实现统一,与操作系统保持一致
眼见为实


https://img2024.cnblogs.com/blog/1084317/202506/1084317-20250625214057324-773247664.png

注意寄存器与栈是两片独立运行的区域,光从汇编代码,很容易陷入误区,就拿上图来说,从上往下阅读汇编,你会发现参数传递的顺序是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下,如果方法的参数 a + b + c + d + e;      // 第5-8个参数通过栈传递            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;            var data5 = new int;            var data8 = new int;            for (int i = 0; i < BenchmarkIterations; i++)            {                for (int j = 0; j < 4; j++) data4 = _random.Next();                for (int j = 0; j < 5; j++) data5 = _random.Next();                for (int j = 0; j < 8; j++) data8 = _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, data4, data4, data4);                Stack1Param(data5, data5, data5, data5, data5);                Stack4Params(data8, data8, data8, data8,                            data8, data8, data8, data8);            }            Console.WriteLine("完成");      }      private static long Measure(Func 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,                        data4,                        data4,                        data4                  );                }            }            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,                        data5,                        data5,                        data5,                        data5                  );                }            }            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,                        data8,                        data8,                        data8,                        data8,                        data8,                        data8,                        data8                  );                }            }            return sum;      }    }
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
页: [1]
查看完整版本: 为什么说方法的参数最好不要超过4个?