【问题标题】:'Correct' unsigned integer comparison“正确”无符号整数比较
【发布时间】:2017-10-19 12:23:18
【问题描述】:

所以,我们都知道 -1 > 2u == true 的 C/C++ 有符号/无符号比较规则,我有一种情况,我想有效地实现“正确”比较。

我的问题是,考虑到人们熟悉的尽可能多的架构,哪种方法更有效。显然英特尔和 ARM 的权重更高。

给定:

int x;
unsigned int y;
if (x < y) {}

推广是不是更好:

x < y  =>  (int64)x < (int64)y

或者执行2个比较更好,即:

x < y  =>  (x < 0 || x < y)

前者意味着零扩展、符号扩展和一个比较+分支,后者不需要符号扩展操作,但需要 2 个连续的 cmp+分支。
传统观点认为分支比符号扩展更昂贵,这两者都会流水线,但在第一种情况下,扩展和单一比较之间存在停顿,而在第二种情况下,我可以想象某些架构可能会流水线化 2 个比较,但随后是2个条件分支?

存在另一种情况,无符号值是比有符号类型更小的类型,这意味着可以通过对有符号类型的长度进行一次零扩展,然后进行一次比较......在这种情况下,是使用extend+cmp 版本更可取,还是仍然首选2-comparison 方法?

英特尔?手臂?其他的? 我不确定这里是否有正确的答案,但我想听听人们的看法。如今,低级性能很难预测,尤其是在 Intel 上,在 ARM 上也越来越多。

编辑:

我应该补充一下,有一个明显的解决方案,其中类型的大小等于架构的 int 宽度;在这种情况下,显然首选 2 比较解决方案,因为促销本身不能有效地执行。显然,我的int 示例满足了 32 位架构的这个条件,您可以将思想实验转换为 short 以进行应用于 32 位平台的练习。

编辑 2:

抱歉,我忘记了-1 &gt; 2u 中的u! >_

编辑 3:

我想修改这种情况,假设比较的结果是一个实际的分支,并且结果不是作为布尔值返回的。这就是我更喜欢结构外观的方式;尽管这确实提出了一个有趣的观点,即当结果是布尔值与分支时,还有另一组排列。

int g;
void fun(int x, unsigned in y) { if((long long)x < (long long)y) g = 10; }
void gun(int x, unsigned in y) { if(x < 0 || x < y) g = 10; }

当您遇到 if 时,这会产生通常暗示的预期分支;)

【问题讨论】:

标签: c++ c assembly type-conversion micro-optimization


【解决方案1】:

好吧,您已经正确地描述了这种情况:C/C++ 无法通过单个比较进行完整的有符号整数/无符号整数比较。

如果升级到 int64 比进行两次比较更快,我会感到惊讶。以我的经验,编译器非常善于意识到这样的子表达式是纯粹的(没有副作用),因此不需要第二个分支。 (您也可以使用按位或显式选择退出短路:(x &lt; 0) | (x &lt; y)。)相比之下,我的经验是编译器往往不会对大于本机字长的整数进行太多特殊情况优化,所以@987654324 @ 很可能实际上会进行完整的 int 比较。

底线,没有咒语可以保证在任何处理器上产生最好的机器代码,但是对于最常见的处理器上最常见的编译器,我猜两个比较形式不会比提升到 int64 形式。

编辑:Godbolt 上的一些讨论证实了在 ARM32 上,GCC 在 int64 方法中投入了太多的机器。 VC 在 x86 上做同样的事情。但是,对于 x64,int64 方法实际上缩短了一条指令(因为提升和 64 位比较是微不足道的)。不过,流水线可能会使实际性能变得更糟。 https://godbolt.org/g/wyG4yC

【讨论】:

  • 这也是我的怀疑,我希望 2-compare 选项在现代系统上更胜一筹......但我之前没有实现过,我相信其他人已经完成了努力让它超时,或者至少,盯着各种拱门的代码。
  • 更少的指令并不一定意味着更快的运行时间
  • @M.M 我知道,这正是为什么我觉得这是一个值得在 SO 上回答的难题。
  • 我应该提一下,我测试了(x &lt; 0 || x &lt; y) 和(x &lt; 0) | (x &lt; y),编译器发出相同的代码。整数提升案例发出不同的代码。令我惊讶的是,编译器无法识别该模式,而只是选择了更有效的实现(可能与上下文相关)。
  • 您的 Godbolt 链接升级为 uint64_t。提升是相同的,但比较谓词不同(如果您想继续将带符号的数字视为潜在负数,则错误)。 AArch64 可以非常有效地做到这一点,将输入的符号扩展作为另一条指令的一部分。 cmp x1, x0, sxtwgodbolt.org/z/iZYYdB。现在是 2019 年(当你回答这个问题时是 2017 年);我认为更多地强调 x86-64 是合适的; 32 位 x86 越来越不重要。 (但 32 位 ARM 对于低端的东西仍然很重要)
【解决方案2】:

通过一个小模板 jiggery-pokery 我认为我们可以在所有场景中自动获得最佳结果:

#include<iostream>
#include<cassert>


template<class T> auto make_unsigned(T i) -> T { return i; }

auto make_unsigned(int i) -> unsigned int {
    assert(i >= 0);
    return static_cast<unsigned int>(i);
}

auto make_unsigned(short i) -> unsigned short {
    assert(i >= 0);
    return static_cast<unsigned short>(i);
}

auto make_unsigned(long long i) -> unsigned long long {
    assert(i >= 0);
    return static_cast<unsigned long long>(i);
}

template<
        class I1,
        class I2,
        std::enable_if_t<(std::is_signed<I1>::value and std::is_signed<I2>::value)
                         or (not std::is_signed<I1>::value and not std::is_signed<I2>::value)>* = nullptr
>
bool unsigned_less(I1 i1, I2 i2) {

    return i1 < i2;
};

template<
        class I1,
        class I2,
        std::enable_if_t<std::is_signed<I1>::value and not std::is_signed<I2>::value>* = nullptr
>
bool unsigned_less(I1 i1, I2 i2) {

    return (i1 < 0) or make_unsigned(i1) < i2;
};

template<
        class I1,
        class I2,
        std::enable_if_t<not std::is_signed<I1>::value and std::is_signed<I2>::value>* = nullptr
>
bool unsigned_less(I1 i1, I2 i2) {

    return not (i2 < 0) and i1 < make_unsigned(i2);
};



int main() {

    short a = 1;
    unsigned int b = 2;

    std::cout << unsigned_less(a, b) << std::endl;

    using uint = unsigned int;
    using ushort = unsigned short;
    std::cout << unsigned_less(ushort(1), int(3)) << std::endl;

    std::cout << unsigned_less(int(-1), uint(0)) << std::endl;
    std::cout << unsigned_less(int(1), uint(0)) << std::endl;

    return 0;
}

【讨论】:

  • 我同意,解决这个问题的实现看起来像这样,但你的回答没有解决这个问题;在特定情况下是 int 提升还是多重比较更有效?您已经进行了多次比较,但这并没有解决问题。
  • @ManuEvans 优化器将看到所有手动优化的尝试。不太可能值得手动优化
  • 它没有。 Optimiser 在我测试过的所有编译器上都清楚地发出了这两种情况。
【解决方案3】:

您必须根据具体情况来判断。在程序中使用有符号类型有几个原因:

  1. 因为您实际上需要在计算或输出中使用负数。
  2. “草率打字”,这意味着程序员只是在整个程序中输入int 而没有考虑太多。
  3. 意外签署。程序实际上并不想要带符号的数字,而是通过隐式类型提升或使用整数常量(例如 0,它的类型为 int)意外地得到了它们。

在 1) 的情况下,应使用带符号的算术进行算术。然后,您应该转换为包含最大预期值所需的最小可能类型。

例如,假设一个值的范围可以从-10000 到10000。然后,您将需要使用 16 位有符号类型来表示它。然后独立于平台使用的正确类型是int_fast16_t。

int_fastn_t 和 uint_fastn_t 类型要求类型至少与 n 一样大,但如果它提供更快的代码/更好的对齐,则允许编译器选择更大的类型。


2) 通过学习stdint.h 并停止懒惰可以治愈。作为一名程序员,总是需要考虑程序中声明的每个变量的大小和符号。这必须在声明时完成。或者,如果您稍后获得某种启示,请返回并更改类型。

如果你不仔细考虑类型,你肯定会写出大量的,通常是微妙的错误。这在 C++ 中尤为重要,C++ 对类型正确性比 C 更挑剔。

当使用“草率打字”时,实际预期的类型通常是无符号的而不是有符号的。考虑这个草率的打字示例:

for(int i=0; i<n; i++)

在这里使用signed int 完全没有意义,那你为什么要这样做呢?您很可能正在遍历数组或容器,然后要使用的正确类型是 size_t。

或者,如果您知道 n 可以容纳的最大大小,例如 100,那么您可以使用最合适的类型:

for(uint_fast8_t i=0; i<100; i++)

3) 也是通过学习治愈的。值得注意的是这些语言中存在的各种隐式提升规则,例如通常的算术转换和整数提升。

【讨论】:

  • “那么你可以使用最合适的类型”这不是一个好主意,int 是定义上平台上最快的类型。
  • @Slava 不,不是。 int 在所有 8 位计算机上都是非常慢的类型。理论上它也可能比 64 位计算机上的 long long 慢。 C 和 C++ 标准没有要求 int 应该对应于最快的类型,实现只是为了方便而倾向于这样做。
  • @Slava 请阅读答案...我不建议使用较小的尺寸,我建议使用可以容纳数字的最快的类型。这提供了最佳的执行速度和内存使用。与 int 不同,它会为您提供一个签名的、不可携带的号码,该号码可能是最快的,也可能不是。
  • @Slava,请注意int 对于在同一台机器上使用相同工具编译的相同代码甚至不一定是相同类型。我使用了允许通过命令行选项选择其宽度的编译器。 C 和 C++ 具有 Lundin 所描述的类型是有充分理由的,他提到了其中的几个。我真的不明白您为什么反对使用使您的代码不仅可证明正确而且快速且可移植的语言功能。
  • 如果您想对最快的计算进行微优化并移植到 8 位或 16 位编译器,有标准类型 int_fast8_t、int_fast16_t 和 int_fast32_t。跨度>
【解决方案4】:

两个分支版本肯定会更慢,但实际上,如果你想要一个布尔整数结果,那么在 x86 上,这些都不是两个分支......也不是单一分支......。

例如 x86-64 gcc 7.1 将用于 C++ 源代码:

bool compare(int x, unsigned int y) {
    return (x < y); // "wrong" (will emit warning)
}

bool compare2(int x, unsigned int y) {
    return (x < 0 || static_cast<unsigned int>(x) < y);
}

bool compare3(int x, unsigned int y) {
    return static_cast<long long>(x) < static_cast<long long>(y);
}

生成此程序集 (godbolt live demo)。 (EDI 和 ESI 中的 Args,上半部分可能是垃圾)

compare(int, unsigned int):
        cmp     edi, esi
        setb    al
        ret

compare2(int, unsigned int):
        mov     edx, edi
        shr     edx, 31       # sign bit of x
        cmp     edi, esi
        setb    al
        or      eax, edx
        ret

compare3(int, unsigned int):
        movsx   rdi, edi       # sign-extend x
        mov     esi, esi       # zero-extend y
        cmp     rdi, rsi
        setl    al
        ret

如果您尝试在更复杂的代码中使用它们,它们会在 99% 的情况下被内联(至少零扩展通常会优化掉,因为 GCC 已经知道它会将值写入 32 位寄存器,隐式将上半部分归零)。

没有分析它只是猜测,但是“凭直觉”我会选择compare3作为“更快”,尤其是在某些代码中乱序执行时(有点有趣,它甚至可以进行适当的 32->64 提升对于 uint 参数,虽然它需要相当多的努力来生成与esi 的上 32b 中的一些混乱相比的代码调用......但它可能会在更复杂的计算中内联时摆脱它,它会注意到参数也已扩展为 uint64,因此 compare3 更简单+更短)。

...正如我在评论中所说,我不会执行我需要的任务,例如,我无法想象在有效数据范围未知的情况下工作,所以对于我工作的任务C/C++ 非常适合,我非常欣赏它的工作方式(&lt; 用于有符号和无符号类型的定义明确,并产生最短/最快的代码,并且发出警告以使我作为程序员负责验证它,并在需要适当更改源的情况下)。

【讨论】:

  • 试试 x86-64 icc。 (说真的,英特尔,WTF。)
  • 我建议您构建基准以生成实际的分支,而不是返回布尔值;我认为这是if 声明的常见结果。
  • 有趣的是,我用 GCC5.4 进行了测试,2-compare 代码更糟糕。似乎代码生成从 GCC6.3 -> 7.1 有所改进。
  • @ManuEvans about if .. 哦,当然我会尽可能地避免分支,但你是对的,这确实改变了一些情况......特别是如果你知道你有很多负 int 值。但是你必须选择你是用一般的方式推理,还是你有特定的任务。对于现实生活中的特定任务,我首先会尝试(非常努力地)避免类型不匹配,因为这对于现实世界的数据通常很可能,并且会简化一切。如果不可能,我会检查您是否可以整体过滤掉负值。上面的代码是最后的手段
  • 在 x86 上 compare2 可能更好,但在 x86_64 上转换为 long long 像 compare3 可能会更好
【解决方案5】:

鉴于您提供的具体设置:

int x;
unsigned int y;

并且您明显打算评估 x 的值在数值上是否小于 y 的值,考虑到 x 的符号,我倾向于将其写为

if ((x < 0) || (x < y)) {}

也就是说,您的第二种选择。它清楚地表达了意图,并且可以扩展到更广泛的类型,只要y的类型的最大可表示值至少与x的类型的最大可表示值一样大。因此,如果您愿意规定参数将采用这种形式,那么您甚至可以将其写成——请注意,C++ 追随者——一个宏。

将两个参数都转换为有符号的 64 位整数类型不是可移植的解决方案,因为不能保证这实际上是来自int 或unsigned int 的促销 .它也不能扩展到更广泛的类型。

至于你的两个替代方案的相对性能,我怀疑有很大的不同,但如果这对你很重要,那么你会想要编写一个仔细的基准测试。我可以想象便携式替代方案比另一个需要多一条机器指令,我也可以想象它需要少一条。只有当这种比较支配了您的应用程序的性能时,一条指令才会以一种或另一种方式产生明显的差异。

当然,这取决于您所介绍的情况。如果您想以任一顺序处理混合有符号/无符号比较,对于许多不同的类型,在编译时进行排序,那么基于模板的包装器可以帮助您解决这个问题(这将解决使用宏的问题),但我认为你是专门询问比较本身的细节。

【讨论】:

  • 我自然也会倾向于这种实现,但我的测试表明它会产生明显更长的代码。我在我的问题中提到,对于 32 位架构,您可以考虑与 16 位类型相关的问题进行思想实验。为了实验,我假设在促销案例中有一种“高效”的促销类型。
  • @ManuEvans,相对性能不能作为思想实验来解决,至少在不指定架构细节的情况下是无法解决的。正如我所看到的,您必须以任何一种方式将两个值都加载到寄存器中。然后一种实现最多执行两个条件跳转指令,而另一种实现最多执行两个(但可能不超过一个)转换指令和一个条件跳转。就这一切都是假设而言,我不准备接受编译器如何发出次优代码的无限变化。
  • 我接受,但我仍然很想看到这个讨论的结果,以及是否会出现与小众案例有关的任何意外情况。
【解决方案6】:

看看 Andrei Alexandrescus 最近在柏林举行的 D 会议上关于 Introspection 设计的主题演讲。

在其中,他展示了如何在 DESIGN 时设计一个检查的 int 类,而他提出的功能之一就是 - 如何比较有符号和无符号。

基本上你需要进行2次比较

如果 (signed_var

【讨论】:

    【解决方案7】:

    您可以做的一个可移植技巧是检查是否可以将两个参数从&lt;stdint.h&gt; 扩大到intmax_t,这是实现支持的最宽的整数类型。您可以检查(sizeof(intmax_t) &gt; sizeof(x) &amp;&amp; sizeof(intmax_t) &gt;= sizeof(y)),如果是,请进行扩大转换。这适用于int 是 32 位宽而 long long int 是 64 位宽的非常常见的情况。

    在 C++ 中,您可以做一些聪明的事情,其中​​您有一个安全比较模板来检查 std::numeric_limits&lt;T&gt; 的参数。这是一个版本。 (在 gcc 或 clang 上使用 -Wno-sign-compare 编译!)

    #include <cassert>
    #include <cstdint>
    #include <limits>
    
    using std::intmax_t;
    using std::uintmax_t;
    
    template<typename T, typename U>
      inline bool safe_gt( T x, U y ) {
        constexpr auto tinfo = std::numeric_limits<T>();
        constexpr auto uinfo = std::numeric_limits<U>();
        constexpr auto maxinfo = std::numeric_limits<intmax_t>();
    
        static_assert(tinfo.is_integer, "");
        static_assert(uinfo.is_integer, "");
    
        if ( tinfo.is_signed == uinfo.is_signed )
          return x > y;
        else if ( maxinfo.max() >= tinfo.max() &&
                  maxinfo.max() >= uinfo.max() )
          return static_cast<intmax_t>(x) > static_cast<intmax_t>(y);
        else if (tinfo.is_signed) // x is signed, y unsigned.
          return x > 0 && x > y;
        else // y is signed, x unsigned.
          return y < 0 || x > y;      
      }
    
    int main()
    {
      assert(-2 > 1U);
      assert(!safe_gt(-2, 1U));
      assert(safe_gt(1U, -2));
      assert(safe_gt(1UL, -2L));
      assert(safe_gt(1ULL, -2LL));
      assert(safe_gt(1ULL, -2));
    }
    

    可以通过改变两行来识别浮点数。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2023-03-12
      • 2012-07-28
      • 1970-01-01
      • 2021-03-28
      • 2014-06-08
      • 1970-01-01
      相关资源
      最近更新 更多