【问题标题】:How to copy 4 bytes of char buffer into long如何将 4 个字节的 char 缓冲区复制到 long
【发布时间】:2017-11-19 20:13:14
【问题描述】:

这是我被问到的一个面试问题:

你有以下代码:

长号; 字符缓冲区[50];

如果 buff 与地址 0 对齐,那么 num 获取 buff 前 4 个字节的最有效方法是什么? 如果我们想在一个特定的地方k(buff[k])得到buff的值,我们该怎么做呢?

是否与内存对齐有关?

问候, 罗恩

【问题讨论】:

  • 我不确定内存对齐是否包括字节序,但如果不是,那么字节序是另一个需要考虑的问题。
  • @hnefatl 不包括字节序
  • 我们是否假设sizeof long == 4
  • 这是一个技巧面试问题吗?因为“最有可能通过幼稚指标最有效的方式,即在幼稚的编译器上”和“根据标准并非完全未定义的行为的单一方式”是两个不同的东西(别名类型转换和@987654322 @, 分别)。所以,我不确定接受采访的机构是否有明确的优先事项。
  • @underscore_d 同意,最好的面试问题邀请讨论,而不是阻止 未定义的行为 答案。将此与对话策略进行比较:“您以前来过这里吗?”回答“不”会阻碍对话。

标签: c memory


【解决方案1】:

首先,请了解该问题旨在询问 C 标准中指定的特性之外的特性。 C 标准没有对效率提出要求,因此任何关于效率的问题都必然是在询问 C 实现,而不是关于 C 标准。面试官并没有探究你对 C 本身的了解;他们正在探究您对现代硬件、编译器等的了解。

正如xvan 的回答中提到的,您可以使用*num = * (long *) buff;考虑到问题中隐含的一些假设,这很有效。为了使其可靠地工作:

  1. long 不能有任何陷阱表示,否则我们必须知道被复制的数据不是陷阱表示。

  2. long 必须是四个字节。

  3. 编译器必须容忍别名。也就是说,不能假设因为buff的元素是char,所以我们不会通过指向long的指针来访问它们。

  4. buff 必须按照问题中的说明进行四字节对齐,否则目标硬件必须支持未对齐的负载。

这些特性在 C 实现中并不少见,尤其是在编译期间选择了相应的选项。这段代码的结果很可能是一个双指令序列,它将四个字节从内存加载到一个寄存器,并将四个字节从一个寄存器存储到内存。这就是我认为面试官在测试你的知识。

但是,这不是一个很好的解决方案。正如Ilja Everilä 在评论中指出的那样,您可以简单地写memcpy(&num, buff, sizeof num);。这是复制字节的正确 C 标准方法,一个好的编译器会优化它。例如,我刚刚在 macOS 10.12.6 上使用 Apple LLVM 8.1.0 编译了此源代码,并带有“-O3 -std=c11 -S”(请求优化的开关、使用 2011 C 标准和汇编代码输出):

#include <stdint.h>
#include <string.h>

void foo(uint32_t *L, char *A)
{
    memcpy(L, A, sizeof *L);
}

生成的例程在通常的例程进入和退出代码之间包含这些指令:

movl    (%rsi), %eax
movl    %eax, (%rdi)

因此,编译器已将memcpy 调用优化为加载指令和存储指令。即使编译器不知道buff 的对齐方式可能是什么。它显然“相信”未对齐的加载和存储在目标架构上执行得相当好,因此它选择直接使用加载和存储指令实现memcpy,而不是显式调用库例程并循环复制四个单独的字节。

如果编译器没有像这样立即优化memcpy 调用,它可能需要一点帮助。例如,如果编译器不知道buff 是四字节对齐的,并且目标硬件不能很好地(或根本不)执行未对齐的四字节加载,那么编译器不会将这个memcpy 优化为加载-存储对。在这种情况下,一些编译器具有语言扩展,可以让您告诉他们指针具有比正常对齐更多的内容,例如 M.M. 提到的 GCC 的 __builtin_assume_aligned()。例如,Apple LLVM,我可以这样做:

typedef char AlignedBuffer[50] __attribute__((__aligned__(4)));

void foo(uint32_t *L, AlignedBuffer *A)
{
    *L = * (long *) A;
}

typedef 告诉编译器AlignedBuffer 类型至少总是四字节对齐的。当然,这是对 C 语言的扩展,并非在所有编译器中都可用。 (另外,在执行此操作时,我必须确保使用支持通过指向其他类型的指针对事物进行别名的编译器选项。)

鉴于这个编译器已经知道如何优化这种情况,试图用指针转换来超越它是没有意义的。但是,当在其他情况下与其他编译器一起工作时,可能需要使用指针转换之类的东西来获得所需的性能。但是需要知道这是依赖于实现的,并且代码应该这样记录,以便其他人知道如果不解决这些问题就不能将其移植到其他 C 实现。

关于后续问题,可以写*num = * (long *) (buff + k);。这个后续问题的重点可能是探究您对硬件对齐要求的了解。在许多系统上,尝试从非四字节对齐的地址加载四字节数据会导致异常。因此,当k 不是四的倍数时,此赋值语句可能会在此类硬件上失败。 (此外,我们应该注意k 必须是所有要加载的字节都在buff 内,或者以其他方式已知可访问。)面试官可能希望您展示这些知识。

通常对于这样的面试问题,面试官想要的不一定只有一个正确答案。大多数情况下,他们希望看到您了解这些问题,对它们有一定的了解,并对解决这些问题的潜在方法有所了解。

【讨论】:

  • 一些编译器具有“断言”指针正确对齐的内在函数,例如gcc __builtin_assume_aligned()
  • @M.M:我实际上用我的编译器尝试过,然后发现它正在优化,所以我删除了它。但我会在答案中提到它。
  • 您的项目符号列表缺少对齐的东西。考虑到足够智能的编译器(尽管 OP 确实提到了对齐),即使在 x86 上,未对齐的数据也会崩溃:D
  • @AnttiHaapala 假设编译器没有错误;如果它在接受没有对齐信息的char * 的情况下为memcpy 生成movl,那么它必须知道通过movl 的未对齐访问不会在该平台上崩溃。
  • @AnttiHaapala:问题中给出了buff 是四字节对齐的事实,所以这不是我们必须做出的额外假设。但我会提到它。
猜你喜欢
  • 1970-01-01
  • 2012-07-17
  • 2011-12-24
  • 1970-01-01
  • 2010-09-27
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多