【问题标题】:How to determine if memory is aligned?如何判断内存是否对齐?
【发布时间】:2010-12-26 06:29:48
【问题描述】:

我是使用 SSE/SSE2 指令优化代码的新手,直到现在我还没有走得太远。据我所知,一个常见的 SSE 优化函数如下所示:

void sse_func(const float* const ptr, int len){
    if( ptr is aligned )
    {
        for( ... ){
            // unroll loop by 4 or 2 elements
        }
        for( ....){
            // handle the rest
            // (non-optimized code)
        }
    } else {
        for( ....){
            // regular C code to handle non-aligned memory
        }
    }
}

但是,我如何正确确定 ptr 指向的内存是否通过例如对齐16 字节?我认为我必须包含非对齐内存的常规 C 代码路径,因为我无法确保传递给此函数的每个内存都将对齐。并且使用内部函数将数据从未对齐的内存加载到 SSE 寄存器似乎非常慢(甚至比常规 C 代码慢)。

提前谢谢你...

【问题讨论】:

  • 随机名称,不确定,但我认为像处理最后几个一样单独处理前几个“未对齐”元素可能更有效。然后你仍然可以将 SSE 用于“中间”...
  • 嗯,这是一个好点。我会试试看。谢谢!
  • 更好:使用标量序言来处理未对齐的元素,直到第一个对齐边界。 (gcc 在使用未知对齐的指针自动矢量化时执行此操作。)或者,如果您的算法是幂等的(如 a[i] = foo(b[i])),则执行潜在未对齐的第一个矢量,然后主循环在第一个对齐边界之后开始向量,然后是在最后一个元素处结束的最终向量。如果数组实际上未对齐和/或计数不是向量宽度的倍数,则其中一些向量将重叠,但仍优于标量。
  • 最佳:提供一个提供 16 字节对齐内存的分配器。然后对 16 字节对齐的缓冲区进行操作,而无需修复前导或尾元素。这就是像 Botan 和 Crypto++ 这样的库为使用 SSE、Altivec 和朋友的算法所做的。

标签: c optimization memory sse simd


【解决方案1】:
#define is_aligned(POINTER, BYTE_COUNT) \
    (((uintptr_t)(const void *)(POINTER)) % (BYTE_COUNT) == 0)

强制转换为void *(或等价的char *)是必要的,因为标准只保证void * 的可逆转换为uintptr_t

如果您想要类型安全,请考虑使用内联函数:

static inline _Bool is_aligned(const void *restrict pointer, size_t byte_count)
{ return (uintptr_t)pointer % byte_count == 0; }

如果byte_count 是编译时常量,则希望编译器优化。

为什么我们需要转换为void *

C 语言允许对不同的指针类型进行不同的表示,例如,您可以有一个 64 位 void * 类型(整个地址空间)和一个 32 位 foo * 类型(一个段)。

转换foo * -> void * 可能涉及实际计算,例如添加偏移量。该标准还让实现将(任意)指针转换为整数时会发生什么,但我怀疑它通常被实现为 noop。

对于这样的实现,foo * -> uintptr_t -> foo * 可以工作,但 foo * -> uintptr_t -> void *void * -> uintptr_t -> @ 987654341@ 不会。对齐计算也不会可靠地工作,因为您只检查相对于段偏移的对齐,这可能是也可能不是您想要的。

结论:始终使用void * 来获得独立于实现的行为。

【讨论】:

  • 这个宏看起来既讨厌又复杂。我一定会测试的。
  • 请提供您知道的任何平台示例,其中non-void * 不会产生uintptr_t 范围内的整数值。和/或,您知道标准如此措辞的理由是什么?
  • 为什么要限制?,看起来只有一个指针时它什么都不做?
  • @Mikhail:const *restrict 的组合比普通的const * 更有保证:没有restrict,丢弃const 并修改内存是合法的; restrict 存在,不是;可悲的是,我了解到这在实践中没有用,因为它只有在实际使用指针时才会生效,而调用者通常无法假设(即有用性仅存在于被调用者方面);在这种特殊情况下,无论如何它都是多余的,因为我们正在处理一个内联函数,因此编译器可以看到它的主体并自行推断没有内存被修改
  • 如果float * 可以(理论上)与void * 有不同的表示,这是否意味着对齐检查可能发生在与预期不同的值上?
【解决方案2】:

编辑:转换为long 是一种廉价的方法来保护自己免受当今最有可能出现的 int 和指针大小不同的可能性。

正如下面的 cmets 所指出的,如果您愿意包含标头,还有更好的解决方案...

((unsigned long)p & 15) == 0 时,指针 p 在 16 字节边界上对齐。

【讨论】:

  • 您可以改用uintptr_t - 可以保证正确的大小来容纳指针。当然,前提是你的编译器定义了它。
  • 指针和整数大小是否不匹配并不重要。您只关心底部的几位。
  • 我通常会使用p % 16 == 0,因为编译器通常和我一样知道 2 的幂,而且我觉得这更易读
  • @Hasturkun 有符号整数的除法/取模在 C99 中不会以按位技巧编译(一些愚蠢的舍入为零的东西),它确实是一个智能编译器,它可以识别取模的结果正在与零进行比较(在这种情况下,按位的东西再次起作用)。不是不可能,但也不是微不足道的。一般来说,如果你想使用 % 并让编译器编译 & ,最好转换为无符号整数。
  • @Pascal Cuoq,gcc 注意到了这一点,并为 (p & 15) == 0(p % 16) == 0 发出完全相同的代码,并设置了 -O 标志。我已经看到许多其他编译器可以识别整数除法/模数/乘法的 2 次方,并对其进行智能处理。 (我确实同意强制转换为未签名)
【解决方案3】:

其他答案建议设置低位并与零进行比较的 AND 操作。

但更直接的测试是使用所需的对齐值进行 MOD,并与零进行比较。

#define ALIGNMENT_VALUE     16u

if (((uintptr_t)ptr % ALIGNMENT_VALUE) == 0)
{
    // ptr is aligned
}

【讨论】:

  • 我支持你,但只是因为你使用的是无符号整数 :)
  • 我相信 uint8_t 类型会失败,有时对齐要求为 1。
  • @jww 我不确定我理解你的意思。对齐要求为 1 意味着基本上没有对齐要求。无需担心uint8_t 的对齐。但如果我理解错了,请澄清。
  • 整数上的u 后缀使其无符号。最好避免在表达式中混合有符号和无符号,以避免混合符号算术可能发生的一些可能的陷阱。请参阅 GCC 警告“有符号和无符号整数表达式之间的比较”。在这种情况下,这可能无关紧要,但养成好习惯是件好事。 (我想0 也应该是0u
  • 请注意,您不应该使用真正的 MOD 操作,这是一项相当昂贵的操作,应尽可能避免。您应该始终使用 and 操作。但我相信,如果您有一个足够复杂的编译器并启用了所有优化选项,它会自动将您的 MOD 操作转换为单个操作码。 (Linux内核使用和操作也仅供参考)
【解决方案4】:

使用类似的函数模板

#include <type_traits>

template< typename T >
bool is_aligned(T* p){
    return !(reinterpret_cast<uintptr_t>(p) % std::alignment_of<T>::value);
}

您可以通过调用类似的方法在运行时检查对齐情况

struct foo_type{ int bar; }foo;
assert(is_aligned(&foo)); // passes

要检查错误对齐是否失败,您可以这样做

// would almost certainly fail
assert(is_aligned((foo_type*)(1 + (uintptr_t)(&foo)));

【讨论】:

  • 这里最好解释一下它是如何工作的,以便 OP 理解它。
  • C++ 明确禁止创建指向给定类型T 的未对齐指针。因为不允许存在这样的指针,所以允许编译器针对任何指针 pis_aligned(p) 优化为 true
  • @paweł-bylica,你可能是对的。您能否提供参考资料(文档、章节、诗句等)以便我修改答案?
  • 同样模板函数总是inline,所以inline关键字是多余的。
  • 那个答案说inline 对显式特化有影响,但显式特化不是模板。该页面上的第二个答案是正确的:stackoverflow.com/a/10535711/1422197 基本上,如果您要将这个模板明确地专门化为一个函数,那么,根据您决定专门化它的位置(例如头文件),您可能需要使用 @ 987654332@ 关键字以避免 ODR 问题,但这总是与您是否在模板上使用 inline 无关。模板上的inline 完全不相关。
【解决方案5】:

这基本上就是我正在使用的。通过将整数作为模板,我确保它的编译时间得到了扩展,因此无论我做什么我都不会以缓慢的模运算结束。

我总是喜欢检查我的输入,因此编译时断言。如果你的对齐值是错误的,那么它就不会编译...

template <unsigned int alignment>
struct IsAligned
{
    static_assert((alignment & (alignment - 1)) == 0, "Alignment must be a power of 2");

    static inline bool Value(const void * ptr)
    {
        return (((uintptr_t)ptr) & (alignment - 1)) == 0;
    }
};

要查看发生了什么,您可以使用以下命令:

// 1 of them is aligned...
int* ptr = new int[8];
for (int i = 0; i < 8; ++i)
    std::cout << IsAligned<32>::Value(ptr + i) << std::endl;

// Should give '1'
int* ptr2 = (int*)_aligned_malloc(32, 32);
std::cout << IsAligned<32>::Value(ptr2) << std::endl;

【讨论】:

    【解决方案6】:

    交给专业人士吧,

    https://www.boost.org/doc/libs/1_65_1/doc/html/align/reference.html#align.reference.functions.is_aligned

    bool is_aligned(const void* ptr, std::size_t alignment) noexcept; 
    

    示例:

            char D[1];
            assert( boost::alignment::is_aligned(&D[0], alignof(double)) ); //  might fail, sometimes
    

    【讨论】:

      【解决方案7】:

      你能用 0x03(在 4 秒对齐)、0x07(在 8 秒对齐)或 0x0f(在 16 秒对齐)“和”ptr,看看是否设置了最低位吗?

      【讨论】:

      • 不,你不能。指针不是 & 运算符的有效参数。
      • @SteveJessop 你可以投到uintptr_t
      • @MarkYisri:是的,我希望在实践中,每个支持 SSE2 指令的实现都提供了一个特定于实现的保证,它可以工作:-)
      【解决方案8】:

      怎么样:

      void *mem = malloc(1024+15); 
      void *ptr =( (*(char*)mem) - (*(char *)mem % 16) );
      

      【讨论】:

      • -1 不回答问题。 (问题是“如何确定内存是否对齐?”,而不是“如何分配一些对齐的内存?”)
      • @milleniumbug 他确实在第二行对齐
      • @MarkYisri 这也不是“如何对齐缓冲区?”
      • @milleniumbug 是否是缓冲区无关紧要。 mem 是一个指针。
      • @MarkYisri 这也不是“如何对齐指针?”。 “mem 对齐了吗?”的答案不是指针。这是“是”或“否”。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2013-05-12
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-07-26
      • 2013-10-31
      • 1970-01-01
      相关资源
      最近更新 更多