【问题标题】:Function pointers in embedded systems, are they useful?嵌入式系统中的函数指针,它们有用吗?
【发布时间】:2011-01-29 10:04:11
【问题描述】:

在一次采访中,他们问我在为嵌入式系统编写代码时使用函数指针是否有益(在速度方面)?我对嵌入式系统一无所知,所以无法回答这个问题。只是一个模糊或模糊的答案。 那么真正的好处是什么?速度、可读性、维护、成本?

【问题讨论】:

  • 面试官显然没有太多嵌入式系统和/或编程方面的经验,无法提出这个问题。好处可能是速度、可读性和维护!尽管“本机”函数指针的语法非常糟糕,您必须将其 typedef 去掉,否则它们将根本不可读。
  • 这个问题实际上可以用两种方式来解读——大多数人认为执行速度是问题所在,但也可以从字面上理解,以了解编写代码速度的好处。如果您将它们用作折叠稍微不同的案例或数据对象之间的共性的手段,那么这可能很重要。部分嵌入式工作需要快速运行;在其他情况下,例如嵌入式 UI,它更多地在于将丰富的功能整合到一个工作中,这通常是一个较短的开发周期。
  • 当面试官给出一个模棱两可的问题时,诀窍是提出聪明的反问来澄清。这可能正是面试官正在寻找的。​​span>
  • 我在这个问题上给了 Chris 和 Clifford 积分,真正的答案是“视情况而定”。您需要通过提出好问题、回答利弊、风险但快速、可维护但缓慢等来找出真正的问题是什么。比特和字节或纳秒差异的详细数量并不像您如何感兴趣那样有趣在有压力的情况下解决问题。

标签: embedded functor


【解决方案1】:

我认为也许 Viren Shakya 的回答错过了面试官试图引出的重点。在某些结构中,使用函数指针可能会加快执行速度。例如,如果你有一个索引,使用它来索引一个函数指针数组可能比一个大开关快。

但是,如果您将静态函数调用与通过指针的调用进行比较,那么 Viren 正确地指出还有一个额外的操作来加载指针变量。但是没有人合理地尝试以这种方式使用函数指针(只是作为直接调用的替代方法)。

通过指针调用函数不能替代直接调用。所以,“优势”的问题是有缺陷的;它们用于不同的环境,通常是为了简化其他代码逻辑和控制流程,而不仅仅是避免静态函数调用。它们的用处在于确定要调用的函数是在运行时由您的代码动态执行的,而不是由链接器静态执行的。从这个意义上说,它们当然在嵌入式系统中很有用,但与嵌入式系统无关。

【讨论】:

  • 不错的答案,通过函数指针区分动态(运行时)决定调用什么。还要指出,函数指针的优点/用途实际上与嵌入式系统本身无关。
  • 感谢 Clifford 的澄清。
【解决方案2】:

有很多用途。

在嵌入式系统中函数指针最重要的一个用途是创建向量表。许多 MCU 架构使用位于 NVM 中的地址表,其中每个地址指向一个 ISR(中断服务程序)。这样的向量表可以用 C 语言编写为函数指针数组。

函数指针对于回调函数也很有用。作为现实世界的一个例子,前几天我正在为片上实时时钟编写驱动程序。芯片上只有一个时钟,但我需要很多定时器。这是通过为每个软件定时器保存一个计数器来解决的,该计数器由实时时钟中断增加。数据类型如下所示:

typedef struct
{
  uint16_t counter;
  void (*callback)(void);

} Timer_t;

当硬件定时器与软件定时器相等时,通过与计数器一起存储的函数指针,调用用户指定的回调函数。像上面这样的东西在嵌入式系统中是很常见的结构。

函数指针在创建引导加载程序等时也很有用,您将在运行时将代码写入 NVM,然后调用它。您可以通过函数指针执行此操作,但绝不可以通过链接函数,因为在链接时代码实际上并不存在。

正如已经提到的,函数指针当然对许多优化很有用,比如优化一个 switch 语句,其中每个“case”是一个相邻的数字。

【讨论】:

    【解决方案3】:

    要考虑的另一件事是,这个问题将是一个很好的机会,可以展示您如何在开发过程中做出设计决策。我可以想象给予的一种回应是转身并考虑您的实施替代方案是什么。从 Casey 和 Lundin 的答案中获取一页,我发现回调函数在将我的模块相互隔离并使代码更改更容易方面非常有用,因为我的代码处于永久原型设计阶段,并且事情变化迅速且经常发生。我目前关心的是易于开发,而不是速度。

    在我的情况下,我的代码通常涉及多个模块,这些模块需要相互发送信号以同步操作顺序。以前,我将其实现为带有外部链接的大量标志和数据结构。有了这个实现,两个问题通常会占用我的时间:

    1. 由于任何模块都可以接触外部变量,因此我将大量时间花在监管每个模块以确保这些变量按预期使用。
    2. 如果其他开发人员引入了新标志,我发现自己在多个模块中潜水,以寻找原始声明和(希望)cmets 中的使用说明。

    使用回调函数,问题就消失了,因为函数变成了信号机制,您可以利用这些好处:

    1. 模块交互由函数接口强制执行,您可以测试前置/后置条件。
    2. 由于回调用作外部模块的接口,因此对全局共享数据结构的需求较少。
    3. 减少耦合意味着我可以相对更轻松地换出代码。

    目前我的性能会受到影响,因为即使有所有额外的函数调用,我的设备仍然可以正常运行。当性能开始成为一个更大的问题时,我会考虑我的替代方案。

    回到面试问题,即使你在技术上可能不精通函数指针的具体细节,但我认为你仍然是一个有价值的候选人,因为你知道你在设计过程。

    【讨论】:

      【解决方案4】:

      您在速度上有所提高,但在可读性和维护方面有所损失。而不是 if-then-else 树,如果 a 然后 fun_a(),else if b 然后 fun_b(),else if c 然后 fun_c() else fun_default(),并且每次都必须这样做,而不是 if a then fun =fun_a, else if b then fun=fun_b, 等等,你这样做一次,从那时起只需调用 fun()。快多了。正如所指出的,你不能内联,这是另一个速度技巧,但在 if-then-else 树上内联并不一定比没有内联更快,而且通常不如函数指针快。

      您会失去一点可读性和维护性,因为您必须弄清楚 fun() 的设置位置、更改频率(如果有的话)、确保在设置之前不要调用它,但它仍然是一个可搜索的名称用于查找和维护所有使用它的地方。

      每次你想要执行一个函数时,它基本上是一种避免 if-then-else 树的速度技巧。如果性能不重要,那么 fun() 可能是静态的并且其中包含 if-then-else 树。

      编辑添加一些例子来解释我在说什么。

      extern unsigned int fun1 ( unsigned int a, unsigned int b ); 无符号整数(* funptr)(无符号整数,无符号整数); void have_fun(无符号整数 x,无符号整数 y,无符号整数 z) { 无符号整数 j; 乐趣=乐趣1; j=fun1(z,5); j=funptr(y,6); }

      编译给出了这个:

      玩得开心: stmfd sp!, {r3, r4, r5, lr} .save {r3, r4, r5, lr} ldr r4, .L2 移动 r5, r1 移动 r0, r2 移动 r1, #5 ldr r2, .L2+4 str r2, [r4, #0] 乐趣1 ldr r3, [r4, #0] 移动 r0, r5 移动 r1, #6 blx r3 ldmfd sp!, {r3, r4, r5, pc}

      我认为克利福德所说的是直接呼叫,如果附近 足够(取决于架构),是一条指令

      乐趣1

      一个函数指针,至少要花你两个

      ldr r3, [r4, #0] blx r3

      我还提到了直接和间接之间的区别是 你产生的额外负担。

      在继续之前,值得一提的是内联的优缺点。 对于这些示例所使用的 ARM,调用 约定使用 r0-r3 作为函数的传入参数和 r0 返回。所以用三个参数进入 have_fun() 意味着 r0-r3 有内容。对于 ARM,还假设一个函数可以破坏 r0-r3,因此 have_fun() 需要保留输入,然后将 r0 和 r1 中 fun1() 的两个输入,因此发生了一些寄存器舞蹈。

      移动 r5, r1 移动 r0, r2 移动 r1, #5 ldr r2, .L2+4 str r2, [r4, #0] 乐趣1

      编译器足够聪明,可以看出我们从来不需要第一个 have_fun() 函数的输入,因此 r0 被丢弃并允许 马上改。编译器也很聪明,知道 发送后我们永远不需要第三个参数 z (r2) 在第一次调用时将其发送到 fun1(),因此无需将其保存在高位 登记。虽然 R1,have_fun() 的第二个参数确实需要 被保存,所以它被放在一个不会被 fun1() 破坏的注册器中。

      你可以看到第二个函数调用发生了同样的事情。

      假设 fun1() 是这个简单的函数:

      内联 unsigned int fun1 ( unsigned int a, unsigned int b ) { 返回(a+b); }

      当你内联 fun1() 时,你会得到这样的结果:

      stmfd sp!, {r4, lr} 移动 r0, r1 移动 r1, #6 添加 r4, r2, #5

      编译器不需要将低位寄存器洗牌 准备打电话。同样,您可能已经注意到 r4 和 当我们进入 hello_fun() 时,lr 被保存在堆栈中。有了这个 ARM 调用约定一个函数可以破坏 r0-r3 但必须保留 所有其他寄存器,因为在这种情况下 have_fun() 需要更多 比四个寄存器做它的事情它保存了 r4 的内容 堆栈以便它可以使用它。同样,我编译的这个函数 它确实调用了另一个函数,bl/blx 指令使用/销毁 lr register (r14) 所以为了让 have_fun() 返回我们也有 将 lr 保留在堆栈上。 fun1() 的简化示例 不显示这一点,但您从内联中获得的另一个节省是在进入时 调用的函数不必设置堆栈帧并保留 寄存器,就好像你从函数中获取代码一样 将其与调用函数内联。

      你为什么不一直内联?首先它可以并且将使用 更多的寄存器,这会导致更多的堆栈使用,而且堆栈很慢 相对于寄存器。最重要的是,它增加了 你的二进制文件的大小,如果 fun1() 是一个大小合适的函数并且你调用了 在 have_fun() 中它 20 次,你的二进制文件会大得多。为了 具有千兆字节内存的现代计算机,几百或几十万 字节没什么大不了的,但是对于资源有限的嵌入式,这可以 成就或毁掉你。在现代千兆赫多核桌面上,多久 无论如何,你需要刮一个或五个指令吗?有时是但 并非所有功能都适用。所以只是因为你可能可以 在您可能不应该在桌面上使用它。

      回到函数指针。所以我试图用我的 答案是,您可能希望在什么情况下使用函数指针 无论如何,用例是什么,在这些用例中有多少 有帮助还是有伤害?

      我想到的案例类型是插件,或者特定于 响应特定硬件的调用参数或通用代码 检测到。例如,一个假设的 tar 程序可能想要输出 到磁带驱动器、文件系统或其他,您可以选择将 具有使用函数指针调用的通用函数的代码。入境时 对于程序,命令行参数指示输出,并且在 那一点您将函数指针设置为特定于设备的 功能。

      如果(outdev==OUTDEV_TAPE)data_out=data_out_tape; 否则如果(outdev==OUTDEV_FILE) { //打开文件等 数据输出=数据输出文件; } ...

      或者也许你不知道你是否在一个处理器上运行 fpu 或您拥有的 fpu 类型,但您知道浮点除法 你想做的事情可以使用 fpu 运行得更快:

      如果(fputype==FPU_FPA)fdivide=fdivide_fpa; 否则如果(fputype==FPU_VFP)fdivide=fdivide_vfp; 否则 fdivide=fdivide_soft;

      而且你绝对可以使用 case 语句代替 if-then-else 树,各有优缺点,一些编译器将 case 语句转换为 int 无论如何都是一棵 if-then-else 树,所以它并不总是重要的。我的重点 试图做的是,如果你这样做一次:

      如果(fputype==FPU_FPA)fdivide=fdivide_fpa; 否则如果(fputype==FPU_VFP)fdivide=fdivide_vfp; 否则 fdivide=fdivide_soft;

      并且在程序的其他任何地方都这样做:

      a=fdivide(b,c);

      与您每次都执行此操作的非函数指针替代方案相比 你想划分的地方:

      if(fputype==FPU_FPA) a=fdivide_fpa(b,c); 否则 if(fputype==FPU_VFP) a=fdivide_vfp(b,c); 否则 a=fdivide_soft(b,c);

      函数指针方法,即使它花费你一个额外的 ldr 在每次通话时,比所需的许多指令便宜很多 if-then-else 树。你先付一点钱来设置 fdivide 指针一次,然后在每个实例上支付额外的 ldr,但总体而言 它比这更快:

      unsigned int fun1 ( unsigned int a, unsigned int b ); unsigned int fun2 (unsigned int a, unsigned int b); unsigned int fun3 (unsigned int a, unsigned int b); 无符号整数(* funptr)(无符号整数,无符号整数); unsigned int have_fun ( unsigned int x, unsigned int y, unsigned int z ) { 无符号整数 j; 开关(x) { 默认: 案例1:j=fun1(y,z);休息; 案例2:j=fun2(y,z);休息; 案例3:j=fun3(y,z);休息; } 返回(j); } unsigned int more_fun ( unsigned int x, unsigned int y, unsigned int z ) { 无符号整数 j; j=funptr(y,z); 返回(j); }

      给我们这个:

      cmp r0, #2 贝克.L3 cmp r0, #3 贝克.L4 移动 r0, r1 移动 r1, r2 乐趣1 .L3: 移动 r0, r1 移动 r1, r2 乐趣2 .L4: 移动 r0, r1 移动 r1, r2 乐趣3

      而不是这个

      移动 r0, r1 ldr r3, .L7 移动 r1, r2 blx r3

      对于默认情况,if-then-else 树会烧掉两个比较和两个 beq 在直接调用函数之前。基本上有时 if-then-else 树会更快,有时函数指针会更快 更快。

      我提出的另一条评论是,如果你使用内联来做到这一点 if-then-else 树更快,而不是函数指针,内联是 总是更快吧?

      unsigned int fun1 ( unsigned int a, unsigned int b ) { 返回(a+b); } unsigned int fun2 ( unsigned int a, unsigned int b ) { 返回(a-b); } unsigned int fun3 ( unsigned int a, unsigned int b ) { 返回(a&b); } unsigned int have_fun ( unsigned int x, unsigned int y, unsigned int z ) { 无符号整数 j; 开关(x) { 默认: 案例1:j=fun1(y,z);休息; 案例2:j=fun2(y,z);休息; 案例3:j=fun3(y,z);休息; } 返回(j); }

      给予

      玩得开心: cmp r0, #2 rsbeq r0, r2, r1 bxeq lr cmp r0, #3 添加 r0, r2, r1 与 r0, r2, r1 bx lr

      大声笑,ARM 让我参与其中。那很好。你可以想象虽然 对于通用处理器,您会得到类似的东西

      cmp r0, #2 贝克.L3 cmp r0, #3 贝克.L4 和 r0,r1,r2 bx lr .L3: 子 r0,r1,r2 bx lr .L4: 添加 r0,r1,r2 bx lr

      你仍然烧掉比较,你拥有的案例越多,时间越长 if-then-else 树。一般情况下不需要太多 比函数指针解决方案更长。

      移动 r0, r1 ldr r1, .L7 ldr r3,[r1] 移动 r1, r2 blx r3

      然后我还提到了可读性和维护性,使用函数 指针方法您需要始终注意是否 函数指针在使用前已分配。你不能 总是只是 grep 为那个函数名并找到你正在寻找的东西 因为在别人的代码中,理想情况下你会找到一个地方 指针已分配,然后您可以 grep 获取真正的函数名称。

      是的,函数指针还有许多其他用例,并且 我所描述的可以通过许多其他方式解决,高效 或不。我试图给海报一些关于如何思考的想法 通过不同的场景。

      我认为这个面试问题最重要的答案不是 有一个正确或错误的答案,因为我认为没有。 但是要看看受访者对编译器做什么或不做什么的了解 做,我上面描述的那种事情。面试题 对我来说是几个问题,你了解编译器实际上是什么吗 确实,它生成什么指令。你明白吗 更少或更多指令不一定更快。你明白吗 不同处理器之间的这些差异,或者你至少有 至少一个处理器的工作知识。然后它继续 可读性和维护性。这是另一个问题 与您阅读他人代码的经验有关,并且 然后维护您自己的代码或其他人的代码。这是一个巧妙的 我认为设计的问题。

      【讨论】:

      • 我怀疑您建议的使用速度是否更快,更不用说“更快”了。第一次调用函数,第二次设置变量然后调用函数;怎么会更快,而且 if-then-else 树根本没有被根除。我建议稍微慢一点。它可能会略微减少代码大小。
      • @Clifford 在一个手臂上,例如,bl r0 在执行时是一个相当快的解决方案,相比之下,如果随机数量的代码然后 bl 地址,否则随机数量的代码然后 bl 地址,否则随机数量代码......我在谈论实际用法。如果直接比较内联与直接使用函数与指向该函数的指针(而不是使用),则速度按顺序排列,内联(执行函数),直接(保存和准备 regs,bl 地址,堆栈内容,函数,堆栈内容, bx lr), 间接 (save and prep regs, ldr rx, address to function, bl rx, stack stuff, function, stack stuff, bx lr)
      • 使用得当,您可以通过使用函数指针来增加可读性和组织性,用深思熟虑的抽象替换选择逻辑。
      • @dwelch:我不相信你的解释,也许如果你发布真实的代码示例来解释你的意思,而不是在文本中“内联”你的代码。在这个解释中,“随机数量的代码”是从哪里突然出现的?
      • @dwelch:我很抱歉,我相信你可能有一个观点,但我不确定你是否表达清楚 - 可能只有我一个人。我觉得如果您将伪代码分离成代码标记块而不是将其埋在文本中,可能会更清楚。一个副作用是,如果您编辑答案,我可以撤回可能仓促的投票。 @Chris Stratton:同意,但答案似乎在两种情况下都给出了相同的条件代码,只是移动了函数调用位置,这是我评论的来源。
      【解决方案5】:

      我会说它们在任何环境中都是有益的(在速度方面),而不仅仅是嵌入式。这个想法是,一旦指针指向正确的函数,就不需要进一步的决策逻辑来调用该函数。

      【讨论】:

      • 可能还不是很清楚,但我认为您的观点是,在某些情况下使用函数指针可以简化或删除选择逻辑。速度不是来自简单地用指针调用替换静态调用;这是不切实际的,正如@Viren 指出的那样,速度并不快。
      【解决方案6】:

      是的,它们很有用。不知道面试官在说什么。基本上,系统是否嵌入是无关紧要的。除非你的筹码非常有限。

      • 速度 不,最快的系统将是单个函数,并且只使用全局变量和 goto 分散在各处。祝你好运。
      • 可读性 是的,这可能会让一些人感到困惑,但总体而言,某些代码使用函数指针更具可读性。它还允许您增加源代码各个方面之间的关注点分离。
      • 可维护性是的,使用函数指针,您将拥有更少的条件、更少的重复代码、更高的代码分离度以及更正交的软件。

      【讨论】:

        【解决方案7】:

        函数指针的一个负面部分是它们永远不会在调用点内联。这可能有益也可能无益,具体取决于您是为速度还是大小进行编译。如果是后者,它们应该与正常的函数调用没有什么不同。

        【讨论】:

          【解决方案8】:

          函数指针的另一个缺点(相对于虚函数,因为它们只是核心级别的函数指针):

          使函数内联 && 虚拟将强制编译器创建同一函数的外联副本。这将增加最终二进制文件的大小(假设已大量使用它)。

          经验法则:1:不要内联进行虚拟呼叫

          【讨论】:

            【解决方案9】:

            这是一个诡计的问题。有些行业是禁止指针的。

            【讨论】:

              【解决方案10】:

              让我们看看...

              速度(假设我们在 ARM 上):那么(理论上):

              (正常函数调用 ARM 指令大小)

              由于它们是设置函数指针调用的额外间接级别,因此将涉及额外的 ARM 指令。

              PS:普通函数调用:用BL设置的函数调用。

              PSS:不知道它们的实际尺寸,但应该很容易验证。

              【讨论】:

              • 我怀疑面试官是否在寻找那个答案。
              • 你为什么一直在这里谈论ARM?原始问题中的什么是指导您针对 ARM 架构定制答案?
              • 在运行良好 C 的嵌入式芯片中,ARM 变体目前确实很流行...
              • 对不起,我只是想以 ARM 为例,也因为它在许多嵌入式系统中被大量使用。
              猜你喜欢
              • 2014-09-08
              • 1970-01-01
              • 2011-09-05
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2012-01-08
              • 2023-03-26
              相关资源
              最近更新 更多