【问题标题】:Is there a way to avoid code duplication in multiple similar functions?有没有办法避免多个类似功能中的代码重复?
【发布时间】:2020-02-13 23:05:31
【问题描述】:

我正在编写一系列函数,这些函数将嵌入一个几乎实时运行的小型微控制器中,因此每个时钟周期都很重要。功能几乎相同。

我能看到在不复制大量代码的情况下做到这一点的唯一方法是使用非常丑陋且不受欢迎的方法在包含文件中声明代码,然后有意包含多次。

以下作品展示了这个概念:

// func.inc

// The absence of an include guard is intentional, as each time this file gets included
// the output will be different

void FUNC(int x)
{
    /* SNIP - lots and lots of code that is duplicated between 
       variant A and B (and more) of the function
    for ( ... 4096 ) 
    {
        for( lots more nested loops)
        {
    */
    // IMPORTANT - I do not want to call functions here as it is
    // in a tight loop withconsecutive memory accesses of 
    // different sizes of strided sparse arrays
#ifdef A
    printf("A %d\n", x);
#endif
#ifdef B
    printf("B %d\n", x);
#endif
    /*
        }
    }
    */
// main.c

#include <stdio.h>

#define FUNC func_A  
#define A
#include "func.inc"
#undef A
#undef FUNC

#define FUNC func_B  
#define B
#include "func.inc"
#undef B
#undef FUNC

#define FUNC func_AB  
#define A
#define B
#include "func.inc"

int main()
{
    func_A(10);
    func_B(20);
    func_AB(30);

    printf("Done\n");

    return 0;
}

我的问题是,虽然这可行,但它看起来很可怕,并且可能会让其他试图理解它的人感到非常困惑。在这种情况下,使用指向函数的指针效率太低,无法成为可行的选择。

是否有任何人都可以建议的解决方案,而无需简单地复制同一函数的几个略有不同的版本?

【问题讨论】:

  • 您使用的编译器是否缺少优化器?优化器将在生成代码时为您内联函数和折叠常量表达式。您不需要跳过所有这些障碍。在最低,从可读且有意义的代码开始,然后检查编译器以最大优化输出的内容。只有当编译器没有生成你认为应该的东西时,才返回并开始摆弄它。
  • 你应该#undef FUNC 这样你就不会收到关于非良性重新定义的警告。对此进行了编辑,这似乎是一种奇怪的表达方式,但仍然如此。
  • @birdwes,你能澄清一下你想要 C 代码还是 C++ 代码?它们不是同一种语言,您需要相应地标记问题。
  • 这没什么意义。 C++ 是一种真正的语言;它不是 C 语言的一组特定于供应商的扩展,因此您不会遇到“可移植性”问题或“意外构造”。 (在 C++ 中有 对构造函数的隐式调用,但您知道它们正在发生,因为您编写了构造函数!)通常,您可以从 C++ 编译器获得 更好 代码与 C 编译器相比,因为您可以利用现代优化器完全可以忽略的代码中的模板元编程和其他免费抽象等功能。
  • 除此之外,要使用哪种语言由您自己决定,但对于 Stack Overflow 问题,您确实需要选择一种。如果您想从询问 C 开始,您可以这样做,然后如果您对 C 的答案不满意,请继续提出关于 C++ 的第二个问题。但是作为一个为嵌入式系统编写信号处理代码作为他们日常工作的人,我真的不认为你的“C-first”方法有任何意义,而且似乎来自一些基本的误解,或者至少没有理解当前技术状态。

标签: c embedded code-duplication


【解决方案1】:

这里并不清楚什么是伪代码和真实代码,但总体而言,您不应该将#define + #undef + #include 用于不同的代码生成目的。 (您可以使用“X 宏”作为最后的手段。这不是一个理想的解决方案,但比这更好。)

“重要 - 我不想在这里调用函数”的解决方案是调用函数。

函数内联已经存在了大约 30 年,而在 20 年前,C 得到了明确的语言支持。现在编译器比程序员更能确定内联的内容。我将使用显式 inline 来做一个示例,只是为了证明调用函数不会影响性能,如果操作正确的话。

使用传统的 C,你会做这样的事情:

#include <stdio.h>

static inline void SNIP (void)
{
  puts(__func__);
}

static inline void A_stuff (int val)
{
  printf("%s %d\n", __func__, val);
}

static inline void B_stuff (int val)
{
  printf("%s %d\n", __func__, val);
}

typedef enum { A=1, B=2 } AB_t;

void func(AB_t ab, int val)
{
  SNIP();

  if(ab & A)
    A_stuff(val);
  if(ab & B)
    B_stuff(val);
}

int main()
{
  func(A, 10);
  func(B, 20);
  func(A|B, 30);

  printf("Done\n");
  return 0;
}

这是明智的解决方案。在生成的机器代码中实际调用的唯一函数是func 和打印函数。

或者,您也可以使用“X 宏”进行代码生成——这些宏的存在仅是为了避免代码重复,而牺牲了可读性。在这里不会真的推荐它,但我会包括一个完整的例子:

#include <stdio.h>

#define FUNC_LIST \
  X(A,  10)       \
  X(B,  20)       \
  X(AB, 30)       \


static inline void SNIP (void)
{
  puts(__func__);
}

static inline void A_stuff (int val)
{
  printf("%s %d\n", __func__, val);
}

static inline void B_stuff (int val)
{
  printf("%s %d\n", __func__, val);
}

static inline void AB_stuff (int val)
{
  A_stuff(val);
  B_stuff(val);
}

#define X(opt, val) void func_##opt (int x) { SNIP(); opt##_stuff(x); }
  FUNC_LIST
#undef X


int main()
{
  #define X(opt, val) func_##opt(val),
    FUNC_LIST
  #undef X

  printf("Done\n");
  return 0;
}

这与原始代码一样难以阅读,除了“X 宏”是 icky 宏技巧以避免代码重复的事实上的标准。

这会像 C++ 模板一样创建多个函数,因此也不理想。

【讨论】:

  • 这增加了“if”语句的开销。条件分支语句占用时钟周期。当它处于紧密嵌套循环的中间时,它们会加起来。
  • @birdwes 它不在中间,它就在它之前。这只是一个声明......几个 CPU 周期 - 它不会导致缓存未命中,它不会影响分支预测,甚至不值得一提。我每天都在使用硬实时系统,事情很少像人们想象的那样重要。大多数情况下,只是有人认为某些代码必须在进行基准测试之前进行优化才能被遗忘,这是一个坏主意。如果这段代码真的有硬实时约束,就会有定时器、超时、中断等。
  • 另外,用函数指针查找表替换这个 if 语句很简单,但是你得到函数调用开销。低端MCU优化不好,高端MCU优化好。
  • @birdwes 底线是:如果不提及任何实时规范,您就无法针对“在通用计算机上尽可能快地进行优化”。您必须针对一个非常具体的系统进行优化,并且您必须对一个例程允许执行多长时间有一个规范——如果它一开始就很关键的话。其他一切都是所谓的“未成熟的优化”,这个例子确实有很多味道。
【解决方案2】:

编辑:这个问题最初是用 C++ 标记的,因此是答案。


制作模板!


// func.hpp

#ifndef FUNCITON_HPP
#define FUNCITON_HPP

enum Specifier : int {
        A = 1 << 0,
        B = 1 << 1,
};


#include <cstdio>

template <auto sp>
void foo(int x)
{
        /* SNIP - lots and lots of code that is duplicated between
           variant A and B (and more) of the function
        for ( ... 4096 )
        {
            for( lots more nested loops)
            {
        */
        // IMPORTANT - I do not want to call functions here as it is
        // in a tight loop withconsecutive memory accesses of
        // different sizes of strided sparse arrays
        if constexpr (static_cast<bool>(sp & Specifier::A)) {
                std::printf("A %d\n", x);
        }
        if constexpr (static_cast<bool>(sp & Specifier::B)) {
                std::printf("B %d\n", x);
        }
}

#endif //!FUNCITON_HPP

然后


// func.cpp
#include "func.hpp"

auto constexpr func_a = foo<Specifier::A>; // Could also use a #define 
auto constexpr func_b = foo<Specifier::B>;
auto constexpr func_ab = foo<Specifier::A | Specifier::B>;


int main()
{
        func_a(1);
        func_b(1);
        func_ab(1);
}

【讨论】:

  • 我在这个答案上花了很多时间,删除“C++”标签会使我的答案无效,这是不受欢迎的。
  • 问题始终是关于 C 的,从修订版 1 开始。标题明确表示需要在 C 中完成。C++ 标记是错误的,并且已被删除,因此它不会导致其他人误入歧途就像你一样。如果您的嵌入式系统附带的编译器不符合标准或不支持您想要的功能,请考虑将其换成现代版本的 GCC 或 Clang。 Gnu 和 LLVM 代码生成器将生成适用于当今使用的几乎任何微架构的机器代码,包括嵌入式系统中的微架构。他们的优化器也很棒。
  • 显式static_cast&lt;bool&gt; 的目的是什么?这种转换应该是隐式的,不是吗?
  • 好的,所以 Clang 给出了一个错误,MSVC 给出了一个警告,而 GCC 什么也没有。 GCC 甚至没有给出-pedantic-errors 的诊断信息。似乎GCC实际上是错误的。不允许缩小转换范围,尽管我认为转换为bool 的标准是一个奇怪的决定。
  • @M.M 是的似乎是这样,但我认为这条规则是不直观的,因为在需要 bool 的上下文中的隐式转换在 C++ 中被如此频繁地使用和惯用。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2021-12-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-12-02
  • 1970-01-01
  • 2022-11-16
相关资源
最近更新 更多