写点什么

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

  • 2025-06-27
    福建
  • 本文字数:5005 字

    阅读完需:约 16 分钟

简介


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


调用协定


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


1、参数传递方式

  • 寄存器传递:将参数存入 CPU 寄存器,速度最快。

  • 栈传递:将参数压入调用栈,再依次从栈中取出,速度最慢

  • 混合传递:前 N 个参数用寄存器,剩余参数用栈,速度适中


2、栈由谁清理

  • Caller 清理:调用函数后由调用方负责恢复栈指针(如 C/C++的__cdecl)。

  • Callee 清理:被调用函数返回前自行清理栈(如 x64 的默认协定)。


3、寄存器如何使用

  • 易变寄存器(Volatile Registers):函数调用时可能被修改的寄存器(如 x64 的RAXRCXRDX),调用方需自行保存这些寄存器的值。

  • 非易变寄存器(Non-Volatile Registers):函数必须保存并恢复的寄存器(如 x64 的RBXRBPR12-R15)。


x86 架构混乱的调用协定


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



眼见为实






可以看到,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 实现了统一。



眼见为实



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 下,有自己独特的调用协定



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


眼见为实





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


/*这种顺序也是正确的,寄存器是寄存器,栈是栈,汇编的顺序不影响他们的位置无关性,因为是两片独立运行的区域*/push 1Ehmov ecx,0Ahpush 28hmov edx,14hpush 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

体验地址:http://www.jnpfsoft.com/?from=001YH

用户头像

还未添加个人签名 2023-06-19 加入

还未添加个人简介

评论

发布
暂无评论
为什么说方法的参数最好不要超过4个?_参数_不在线第一只蜗牛_InfoQ写作社区