写点什么

“干净”的代码,贼差的性能

  • 2023-03-02
    北京
  • 本文字数:9592 字

    阅读完需:约 31 分钟

“干净”的代码,贼差的性能

很多程序员还是一个“小萌新”时就听过这样的说法:写出来的代码必须得“干净”,为此很多人做了大量的阅读和学习。


Redux 作者 Dan Abramov 就曾痴迷于“干净代码”和删除重复代码。多年前他和同事一起开发一个图形编辑器画布,当看到同事提交代码时,他吐槽道,“这些重复代码看起来真的很碍眼。”随后,他自己想办法把重复的代码删掉了。


“夜已深,我把改好的代码提交到 master 分支,然后上床睡觉。因为帮同事把杂乱的代码清理干净了,我心里还引以为豪。”但事实并不像他想象的美好,第二天老板看到后找他谈话,希望他代码回滚回去。


当时的 Dan 很不理解,直到再工作了几年后他才明白,除了团队协作方面考虑,他为了减少重复代码牺牲了灵活性。“这算不上是一个好的权衡。”他坦诚道。


无独有偶,专门从事游戏引擎研发的资深开发者 Casey Muratori 近日也发表文章称,那些所谓“干净”代码的规则“其实挺无所谓的,多数情况下也不太影响代码的实际运行。”


这是 Casey 亲自测试的结果,他表示,“认真分析就会发现,其中很多要求设置得相当随意,难以证实或证伪。但也有一些则非常‘万恶’,确实会影响到代码的运行效果。”我们将 Casey 的测试分享做了翻译,以飨读者。

“干净代码”的性能测试


下面来看几条有代表性的“干净”建议:


• 相较于“if/else”和“switch”,尽量用多态;


• 不要告诉代码它所处理的对象内部;


• 函数应该小一点;函数应该只做一件事;


• “DRY”——别重复自己。


这些要求相当具体,听起来只要照着做了,就让编写出“干净”的代码。但问题是,这样的代码执行起来效果如何?


为了更确切地测试“干净”代码的实际表现,我决定直接用相关文献里列出的示例代码。这样大家就不能说我故意黑了吧,这里只是用人家提供的现成结果来评估“干净”代码到底能不能打。

尽量用多态?


相信很多朋友都见过如下“干净”代码实例:


/* ========================================================================   LISTING 22   ======================================================================== */
class shape_base{public: shape_base() {} virtual f32 Area() = 0;}; class square : public shape_base{public: square(f32 SideInit) : Side(SideInit) {} virtual f32 Area() {return Side*Side;} private: f32 Side;}; class rectangle : public shape_base{public: rectangle(f32 WidthInit, f32 HeightInit) : Width(WidthInit), Height(HeightInit) {} virtual f32 Area() {return Width*Height;} private: f32 Width, Height;}; class triangle : public shape_base{public: triangle(f32 BaseInit, f32 HeightInit) : Base(BaseInit), Height(HeightInit) {} virtual f32 Area() {return 0.5f*Base*Height;} private: f32 Base, Height;}; class circle : public shape_base{public: circle(f32 RadiusInit) : Radius(RadiusInit) {} virtual f32 Area() {return Pi32*Radius*Radius;} private: f32 Radius;};
复制代码


这是一个基础类,能提供几种特定形状:圆形、三角形、矩形、正方形。之后,它还提供一个用于计算面积的虚拟函数。


跟之前的要求一样,这里用的是多态,函数小而且只做一件事,总之完全符合规定。于是,我们最终得到了非常“干净”的类层次结构。每个派生的类都知道如何计算自己的面积,并存储面积计算所需要的数据。


如果我们想要实际应用这个层次结构,比如想求输入的所有形状的面积总和,那大概应该是这样:


/* ========================================================================   LISTING 23   ======================================================================== */ f32 TotalAreaVTBL(u32 ShapeCount, shape_base **Shapes){    f32 Accum = 0.0f;    for(u32 ShapeIndex = 0; ShapeIndex < ShapeCount; ++ShapeIndex)    {        Accum += Shapes[ShapeIndex]->Area();    }        return Accum;}
复制代码


大家可能注意到了,我在这里没有使用迭代器,因为“干净”规则里并没有建议要使用迭代器。为了避免对编译器的混淆和对性能差异造成的影响,这里我决定不引入任何抽象迭代器。


另外,这个循环还基于一系列指针。这是使用类层次结构所带来的直接结果:我们不知道这些形状在内存里有多大,所以除非添加另外一个虚拟函数调用来获取各形状的数据大小、并引入某种可变的跳过操作,否则就必须要靠指针来找到各个形状的实际起始位置。


这里做的是累加计算,所以会存在循环依赖性,这会导致循环速度下降。为了能随意对累加进行重新排序,我还编写了一个手填版本以确保安全:


/* ========================================================================   LISTING 24   ======================================================================== */ f32 TotalAreaVTBL4(u32 ShapeCount, shape_base **Shapes){    f32 Accum0 = 0.0f;    f32 Accum1 = 0.0f;    f32 Accum2 = 0.0f;    f32 Accum3 = 0.0f;        u32 Count = ShapeCount/4;    while(Count--)    {        Accum0 += Shapes[0]->Area();        Accum1 += Shapes[1]->Area();        Accum2 += Shapes[2]->Area();        Accum3 += Shapes[3]->Area();                Shapes += 4;    }        f32 Result = (Accum0 + Accum1 + Accum2 + Accum3);    return Result;}
复制代码


如果只对这两个例程做简单测试,我们就能粗略测量出每个形状完成计算所消耗的 CPU 时钟周期:



这里用两种不同方式进行代码测试。第一种是仅运行一次,表达“冷”状态下的计算情况——这时数据应存留于 L3 缓存内,但 L2 和 L1 已被刷新清空,而且分支预测变量也尚未在循环中“预演”过。


第二种则是多次运行代码,查看缓存和分支预测变量都“热”着的时候,循环性能如何。请注意,我的这些办法都不是真正的精准测量。大家也能看到,其中的差异如此巨大,压根就没必要使用严肃的分析工具。


从结果来看,这两个例程没有太大区别。“干净”代码在计算形状面积时大概消耗了 35 个计算周期,如果运气好,有时候是 34 个。也就是说,如果严格按照“干净”编程的原则处理,那我们要用掉 35 个计算周期。


可如果不管第一条规矩,结果会怎样?这里我们不使用多态,直接上 switch 语句。


我在这里编写了完全相同的代码,只是不再采取类层次结构的形式(也就是运行时上的 vtable),而是通过枚举和形状类型把所有内容都塞进了单一结构:


/* ========================================================================   LISTING 25   ======================================================================== */ enum shape_type : u32{    Shape_Square,    Shape_Rectangle,    Shape_Triangle,    Shape_Circle,        Shape_Count,}; struct shape_union{    shape_type Type;    f32 Width;    f32 Height;}; f32 GetAreaSwitch(shape_union Shape){    f32 Result = 0.0f;        switch(Shape.Type)    {        case Shape_Square: {Result = Shape.Width*Shape.Width;} break;        case Shape_Rectangle: {Result = Shape.Width*Shape.Height;} break;        case Shape_Triangle: {Result = 0.5f*Shape.Width*Shape.Height;} break;        case Shape_Circle: {Result = Pi32*Shape.Width*Shape.Width;} break;                case Shape_Count: {} break;    }        return Result;}
复制代码


这就是我们被“干净”代码忽悠之前,那种最老派的编程方式。


请注意,因为这里不再为各种形状变体指定相应的数据类型,所以如果类型不具备所讨论的某个值(例如「高度」),则直接忽略。


现在,这段代码不再从虚拟函数调用中获取面积,而是通过 switch 语句从函数中获取——这跟“干净”编程的原则完全不符。但大家应该看得出来,后面这种更简洁,而且代码并没多大变化。Switch 语句的每种执行情况,都跟类层次结构中的相应虚拟函数有着相同的代码。


至于加和循环本身,跟“干净”版本也几乎相同:


/* ========================================================================   LISTING 26   ======================================================================== */ f32 TotalAreaSwitch(u32 ShapeCount, shape_union *Shapes){    f32 Accum = 0.0f;        for(u32 ShapeIndex = 0; ShapeIndex < ShapeCount; ++ShapeIndex)    {        Accum += GetAreaSwitch(Shapes[ShapeIndex]);    }     return Accum;} f32 TotalAreaSwitch4(u32 ShapeCount, shape_union *Shapes){    f32 Accum0 = 0.0f;    f32 Accum1 = 0.0f;    f32 Accum2 = 0.0f;    f32 Accum3 = 0.0f;        ShapeCount /= 4;    while(ShapeCount--)    {        Accum0 += GetAreaSwitch(Shapes[0]);        Accum1 += GetAreaSwitch(Shapes[1]);        Accum2 += GetAreaSwitch(Shapes[2]);        Accum3 += GetAreaSwitch(Shapes[3]);                Shapes += 4;    }        f32 Result = (Accum0 + Accum1 + Accum2 + Accum3);    return Result;}
复制代码


唯一的区别,就是我们在这里没有调用成员函数来获取面积,而是调用了一个正则函数。就这么点不同。


但很明显,与类层次结构相比,扁平结构是有很多好处的:形状都在矩阵里,根本不需要指针。而且因为所有形状的大小都相同,所以也不需要其他间接转换。


另外,编译器现在可以准确理解我们在循环中的操作,即查看 GetAreaSwitch 函数并查看整个代码路径。这样,编译器就用不着对只向运行时开放的虚拟面积函数做操作猜测。


那这些好处到底会在编译器里转化成怎样的效果?这里我们一口气把运行四种形状,结果是:



通过观察结果,我们会发现一些很有趣的现象。单单把代码改得“老派”一点,我们就让性能提升了 1.5 倍。是的,别用 C++ 多态这种无关紧要的东西,性能马上就有了改善。


通过违反“干净”代码原则的头一条(也是比较核心的一条),我们把各形状面积计算的时钟周期从 35 个降低到 24 个。如果要拿硬件做比较,就相当于是 iPhone 14 Pro Max 降级成了 iPhone 11 Pro Max。这是三到四年的硬件演化进程,只靠不用多态就给消弭掉了。


但这还只是刚刚开始。

忽略对象内部?


如果我们违反更多规矩,会怎么样?比如说去掉第二条,“忽略对象内部”。我们能不能靠内部知识帮函数提高运行效率?


回顾一下计算面积的 switch 语句,我们会发现所有面积计算用的都是相似的方法:


        case Shape_Square: {Result = Shape.Width*Shape.Width;} break;        case Shape_Rectangle: {Result = Shape.Width*Shape.Height;} break;        case Shape_Triangle: {Result = 0.5f*Shape.Width*Shape.Height;} break;        case Shape_Circle: {Result = Pi32*Shape.Width*Shape.Width;} break;
复制代码


也就是都在用高度乘以高度、宽度乘以宽度,需要时再乘个π之类的系数。如果是圆,那就除以 2。


这就是我跟“干净”代码原则最不对付的地方,我觉得 switch 语句很棒!它能向我们清晰地展示这些模式,因为在按操作(而不是按类型)进行代码组织时,可以很直观地发现其中的常规模式。相比之下,再看“干净”编程示例,我们可能永远发现不了这样的模式。那边不仅样板更多,而且倡导者建议把每个类都放进单独的文件里。


所以从结构上讲,我一般不赞成使用类层次结构。总而言之,现在我想强调最重要的一点——我们可以通过观察模式,来大大简化这条 switch 语句。


请记住:这个示例不是我选的。这是“干净”代码自己选的说明示例。而且跟面积计算类似,其他很多任务也有相似的算法结构。要想利用这种模式,我们可以整理一个简单的表,用于说明每种类型所对应的系数。如果我们将圆形和矩形等设定为单参数类型,就可以写出更简单的求面积函数:


/* ========================================================================   LISTING 27   ======================================================================== */ f32 const CTable[Shape_Count] = {1.0f, 1.0f, 0.5f, Pi32};f32 GetAreaUnion(shape_union Shape){    f32 Result = CTable[Shape.Type]*Shape.Width*Shape.Height;    return Result;}
复制代码


这里的两个求和循环不用做多大修改,除了只能调用 GetAreaUnion(而非 GetAreaSwitch),其余部分完全相同。


下面来看看这个版本的运行性能如何:



可以看到,通过对实际类型的理解,我们有效将基于类型的思路转换成了基于函数的思路,从而大大提高了速度。跟之前的 iPhone 相比,现在我们的计算速度已经相当于登陆了台式机。


而我们唯一所做的,就是一次表查找加一行代码,没别的了!这样不仅更快,在语义上也更简单。它涉及的 token 更少、操作更少、代码行数也更少。


所以说,我们有必要把数据模型跟计算操作结合起来,而不是要求什么“忽略内部”。现在,我们对每个形状的面积计算只消耗 3.0 到 3.5 个计算周期。


放弃前两条“干净”编程规则,已经让我们的代码性能提升了 10 倍。


10 倍性能提升绝对非同小可,毕竟就连多年之前推出的 iPhone 6(现代性能基准测试所能支持的最老机型),其性能也只是 iPhone 14 Pro Max 的三分之一。


如果用单线程桌面 CPU 性能来比较,那 10 倍的差距就相当于拿现在的 CPU 跟 2010 年的产品对抗。看到了吧,单是前两条“干净”编程规则,就消灭了这 12 年来的硬件演变成果。

函数应该小一点、专一点?


更令人震惊的是,恢复这部分性能的操作如此简单。这里我们没有强调“函数要小”和“函数只做一件事”这两条,毕竟我们这个测试很简单,天然符合这些规定。那么,如果我们在问题里再加个要求,应该就能看到它们的实际影响了吧?


这里,我在原有层次结构之上又添加了一个虚拟函数,用于给出各个形状有几个角:


/* ========================================================================   LISTING 32   ======================================================================== */ class shape_base{public:    shape_base() {}    virtual f32 Area() = 0;    virtual u32 CornerCount() = 0;}; class square : public shape_base{public:    square(f32 SideInit) : Side(SideInit) {}    virtual f32 Area() {return Side*Side;}    virtual u32 CornerCount() {return 4;}    private:    f32 Side;}; class rectangle : public shape_base{public:    rectangle(f32 WidthInit, f32 HeightInit) : Width(WidthInit), Height(HeightInit) {}    virtual f32 Area() {return Width*Height;}    virtual u32 CornerCount() {return 4;}    private:    f32 Width, Height;}; class triangle : public shape_base{public:    triangle(f32 BaseInit, f32 HeightInit) : Base(BaseInit), Height(HeightInit) {}    virtual f32 Area() {return 0.5f*Base*Height;}    virtual u32 CornerCount() {return 3;}    private:    f32 Base, Height;}; class circle : public shape_base{public:    circle(f32 RadiusInit) : Radius(RadiusInit) {}    virtual f32 Area() {return Pi32*Radius*Radius;}    virtual u32 CornerCount() {return 0;}    private:    f32 Radius;};
复制代码


矩形有四个角,三角形有三个角,圆形一个角都没有。之后,我要调整问题的定义,从计算各形状的总面积转为计算各形状的角加权面积和——也就是总面积再加上角总数。


跟总面积一样,算这个角加权面积没有任何实际意义,单纯是为了演示性能差异,用的也是最简单的数学计算。


这里,我用数学计算和其他虚拟函数调用更新了“干净”求和循环:


f32 CornerAreaVTBL(u32 ShapeCount, shape_base **Shapes){    f32 Accum = 0.0f;    for(u32 ShapeIndex = 0; ShapeIndex < ShapeCount; ++ShapeIndex)    {        Accum += (1.0f / (1.0f + (f32)Shapes[ShapeIndex]->CornerCount())) * Shapes[ShapeIndex]->Area();    }        return Accum;} f32 CornerAreaVTBL4(u32 ShapeCount, shape_base **Shapes){    f32 Accum0 = 0.0f;    f32 Accum1 = 0.0f;    f32 Accum2 = 0.0f;    f32 Accum3 = 0.0f;        u32 Count = ShapeCount/4;    while(Count--)    {        Accum0 += (1.0f / (1.0f + (f32)Shapes[0]->CornerCount())) * Shapes[0]->Area();        Accum1 += (1.0f / (1.0f + (f32)Shapes[1]->CornerCount())) * Shapes[1]->Area();        Accum2 += (1.0f / (1.0f + (f32)Shapes[2]->CornerCount())) * Shapes[2]->Area();        Accum3 += (1.0f / (1.0f + (f32)Shapes[3]->CornerCount())) * Shapes[3]->Area();                Shapes += 4;    }        f32 Result = (Accum0 + Accum1 + Accum2 + Accum3);    return Result;}
复制代码


基本上就是整体接入另一个函数,添加了新的间接层。同样是出于明确起见,这里不用任何抽象。


在 switch 语句那边,我做的变更也基本相同。先是给角数量添加另一条 switch 语句,跟层次结构版本可以说是完美对应:


/* ========================================================================   LISTING 34   ======================================================================== */ u32 GetCornerCountSwitch(shape_type Type){    u32 Result = 0;        switch(Type)    {        case Shape_Square: {Result = 4;} break;        case Shape_Rectangle: {Result = 4;} break;        case Shape_Triangle: {Result = 3;} break;        case Shape_Circle: {Result = 0;} break;                case Shape_Count: {} break;    }        return Result;}
复制代码


下面看看这两个版本的计算性能差异:


/* ========================================================================   LISTING 35   ======================================================================== */ f32 CornerAreaSwitch(u32 ShapeCount, shape_union *Shapes){    f32 Accum = 0.0f;        for(u32 ShapeIndex = 0; ShapeIndex < ShapeCount; ++ShapeIndex)    {        Accum += (1.0f / (1.0f + (f32)GetCornerCountSwitch(Shapes[ShapeIndex].Type))) * GetAreaSwitch(Shapes[ShapeIndex]);    }     return Accum;} f32 CornerAreaSwitch4(u32 ShapeCount, shape_union *Shapes){    f32 Accum0 = 0.0f;    f32 Accum1 = 0.0f;    f32 Accum2 = 0.0f;    f32 Accum3 = 0.0f;        ShapeCount /= 4;    while(ShapeCount--)    {        Accum0 += (1.0f / (1.0f + (f32)GetCornerCountSwitch(Shapes[0].Type))) * GetAreaSwitch(Shapes[0]);        Accum1 += (1.0f / (1.0f + (f32)GetCornerCountSwitch(Shapes[1].Type))) * GetAreaSwitch(Shapes[1]);        Accum2 += (1.0f / (1.0f + (f32)GetCornerCountSwitch(Shapes[2].Type))) * GetAreaSwitch(Shapes[2]);        Accum3 += (1.0f / (1.0f + (f32)GetCornerCountSwitch(Shapes[3].Type))) * GetAreaSwitch(Shapes[3]);                Shapes += 4;    }        f32 Result = (Accum0 + Accum1 + Accum2 + Accum3);    return Result;}
复制代码


跟之前的求总面积类似,类层次结构和 switch 两种实现之间的代码基本相同。唯一的区别,就是调用虚拟函数还是使用 switch 语句。


再来看表驱动的示例,这种把计算操作跟数据结合起来办法真的棒。而且这个版本需要修改的只有表里的值。我们甚至不需要获取关于形状的其他信息,只要把角数跟面积系数直接加进表中,就能用几乎相同的代码得出结果:


/* ========================================================================   LISTING 36   ======================================================================== */ f32 const CTable[Shape_Count] = {1.0f / (1.0f + 4.0f), 1.0f / (1.0f + 4.0f), 0.5f / (1.0f + 3.0f), Pi32};f32 GetCornerAreaUnion(shape_union Shape){    f32 Result = CTable[Shape.Type]*Shape.Width*Shape.Height;    return Result;}
复制代码


如果运行所有“角面积”函数,就能看到第二个形状的属性如何影响其性能:



可以看到,这次测试中“干净”代码的表现更差。Switch 语句的性能达到了“干净”版本的 2 倍,而查表版本更是达到后者的 15 倍。


这也凸显出“干净”代码的深层次问题:需求越复杂,这些规矩就越有损性能。当我们把这种“干净”编程方法引入各种真实用例时,最终性能肯定会大打折扣。


而且“干净”代码用得越多,编译器就越理解不了你想干什么。一切都被放进了单独的翻译单元,被藏在虚拟函数调用之后。这样即使编译器再聪明,也难以消化这混乱的实现。


更可怕的是,这样的代码连人看了都会束手无策!从之前的演示中可以看到,如果代码库围绕着函数进行架构设计,那么从表中取值或者删除 switch 语句等需求才会易于实现;而如果是围绕类型进行架构设计,那难度将大大增加。唯一的解决办法,恐怕就只有大规模重写。


总之,只是在形状计算中增加了一个属性,速度差就从 10 倍变成了 15 倍,相当于硬件性能从 2023 年一下子倒退回了 2008 年!一个参数,抹灭 14 年硬件发展,是不是很大胆?而且,咱们还完全没涉及优化呢。


之前的所有演示,都只是在拿循环依赖关系做文章,完全没提有哪些优化空间。下面,我们来看相同计算流程在经过轻度优化后的 AVX 版本:



速度差异到了 20 到 25 倍区间。当然,AVX 优化的代码完全不理会“干净”编程的那些奇谈怪论。五大原则已经祛魅了四条,再来看最后一条。

不要重复自己?


老实讲,“不要重复自己”其实是有道理的。我们拿来测试的版本也没有多少重复部分。只有 4 次累加的部分算是重复,但这是为了演示。毕竟如果是在真实应用当中,我们甚至没必要把它分成 2 个例程。


如果把“不要重复自己”说得更具体点,比如不要把相同系数的两个编码版本分别构建成两个表,那我还可以反对一下。毕竟有时候这样能获得更好的性能。但人家没那么讲,只是说别自我重复,那这话还是相当合理的。


最重要的是,我们完全可以在遵循第五条的同时保持合理的代码性能。

结   论


所以我现在给出结论:在这五条原则里,只有最后一条值得遵循,前面四条可以统统无视。为什么?大家可能注意到了,现在的软件运行起来真的越来越慢。跟现代硬件的真实性能相比,软件的运行表现太差了。


要问为什么这么慢,那答案可就多了,而最核心的因素要视实际开发环境和编程方法而定。但至少从特定角度出发,“干净”代码绝对有着不可推卸的责任。虽然其底层逻辑都说得通,但造成的性能负担却是我们难以承受的。


所以面对这种种规矩,尽管有人认为这样能改善代码库的可维护性,但我们至少也该想想背后的代价是什么。


我们真的愿意放弃这十几年的硬件发展,只为让程序员的工作变得更轻松一点吗?我们的职责就是开发出能顺畅在硬件上运行的程序。如果这些原则严重影响了软件的运行效果,那岂不背离了我们的从业初衷?


当然,我们仍然可以继续探索更好的代码组织、维护改进和易读性方法,这些都是非常合理的诉求。但“干净”编程的这些规矩不是,它们根本就不靠谱。我强烈建议他们能用大星号标明“采取这些规则,您的代码性能将缩水十几倍”。


你选择干净的代码还是不错的性能?欢迎在评论区留下你的看法~


原文链接:


https://www.computerenhance.com/p/clean-code-horrible-performance


2023-03-02 14:565110

评论 9 条评论

发布
用户头像
你凭撒就觉得这是个选择题呢 我都要不行吗
2024-07-10 09:08 · 浙江
回复
用户头像
分需求,看场景呗。开发电子游戏,这样的说法有道理;对于大量的CRUD业务,随便一个IO瓶颈就是上千上万倍的性能差距,优化10倍的CPU等于没有优化;对于有以上性能优化价值,但又存在长期升级维护需求的软件,比如REDIS之类的,也得权衡这几个原则放弃一定的性能能吧
2023-03-07 10:45 · 云南
回复
用户头像
分语言吧,“干净”的原则主要针对维护性和扩展性,具体性能影响多少还是看具体情况,而不能一味的说设计原则都不靠谱。多数情况下这点性能都微不足道,如果有影响可以考虑不遵循。作者太偏激
2023-03-06 15:57 · 北京
回复
用户头像
不用虚函数不就行了?
2023-03-03 22:36 · 湖南
回复
用户头像
以前的程序员为了性能和内存,能做出来一款几十KB的游戏,比如马里奥,现在的全部都在堆内存,试图通过内存来解决体积大的问题,从来也不考虑怎样优化体积的问题,这其实是一个本质的改变,不科学,不可取。
2023-03-03 14:30 · 北京
回复
用户头像
把需要复杂计算抽象出来 把反射资源处理等消耗资源的次数尽量减少 或者异步处理
2023-03-03 09:47 · 广东
回复
用户头像
代码整洁主要还是从可维护性和可扩展性来考虑吧,额外的封装肯定会会带来一定的性能损耗,我觉得这点损耗对 99%的公司都不存在问题,但对长期维护和扩展带来的收益是巨大的。
2023-03-03 08:22 · 广东
回复
用户头像
有没有可能是这个代码中的计算逻辑太简单了,导致这点差异喧宾夺主了
2023-03-02 19:17 · 湖北
回复
用户头像
专注于计算功能的代码, 没谁自讨没趣, 为了所谓的干净, 把switch改成虚函数吧?
2023-03-02 15:39 · 广东
回复
没有更多了
发现更多内容

[SpringBoot]配置文件格式、yaml配置及读取

十八岁讨厌编程

Java 9月月更

[SpringBoot]多环境配置,配置文件分类

十八岁讨厌编程

Java 后端开发 9月月更

每日算法刷题Day1-隐式转换与精度丢失

timerring

算法题 9月月更

在家学习如何保持高度自律

大数据搬运工

学习方法

2022-09-24:以下go语言代码输出什么?A:1;B:3;C:13;D:7。 package main import ( “fmt“ “io/ioutil“ “net/

福大大架构师每日一题

golang 福大大 选择题

探索AI技术应用场景

felix

产业落地 AI探索 API接口 模型管理

关爱2700多万听障者,手语服务助力无声交流

HarmonyOS SDK

手语

面向深度神经网络的特定领域架构

俞凡

深度学习 架构 TPU

Identity and Access Management

冯亮

DevOps security AWS Cloud

开发者有话说|如何写出更加优雅的代码

闫同学

个人成长

死锁检测实现

C++后台开发

后台开发 线程 多线程 死锁 C++开发

工赋开发者社区 |【数智化】数字化工厂规划与建设方案

工赋开发者社区

跟着卷卷龙一起学Camera--内存池浅析01

卷卷龙

ISP 9月月更

新书上市|听说你翻开数学书就眼睛疼?

图灵教育

数学 科普 教育

深入了解之链接器与加载器

邱学喆

加载器 链接器 ELF文件结构

新书上市|听说你翻开数学书就眼睛疼?

图灵社区

数学 科普 教育

流程图布局在项目中的实践

相续心

openEuler资源利用率提升之道 03:rubik混部引擎简介

openEuler

Linux 开源 cpu 操作系统 openEuler

如何在笔记本上安装openEuler 22.03 LTS

openEuler

开源 操作系统 openEuler 安装部署

leetcode 669. Trim a Binary Search Tree 修剪二叉搜索树 (简单)

okokabcd

LeetCode 算法与数据结构

开发者有话说|一名普通大专学历开发者的成长

彭发红

NestOS应用案例:容器化部署OpenStack

openEuler

架构 openEuler 开源操作系统 OpenStack

架构实战营-模块一作业

Geek_92ba6f

【jvm】通过JDBC为例谈谈双亲委派模型的破坏

石臻臻的杂货铺

JVM 9月月更

清览题库--C语言程序设计第五版编程题解析(2)

吉师职业混子

9月月更

算法基础(四)| 前缀和算法及模板详解

timerring

算法 9月月更

闲着刷题

吉师职业混子

9月月更

基于微服务的应用性能监控方案

穿过生命散发芬芳

9月月更 微服务监控

脑机接口照进现实:5位脑科学家带来的最新启示

脑极体

物联网实践分享

彭发红

【云原生 | 从零开始学Kubernetes】七、Kubernetes的命名空间

泡泡

Docker 云计算 容器 云原生 9月月更

“干净”的代码,贼差的性能_语言 & 开发_Casey Muratori_InfoQ精选文章