【问题标题】:How can I elide a call if an edge condition is known at compile time?如果在编译时知道边缘条件,我该如何取消调用?
【发布时间】:2011-12-01 06:21:54
【问题描述】:

我有以下情况:有大量模板,例如 std::vector,它们会调用 memmove() 来移动数组的一部分。有时他们会想要“移动”长度为零的部分——例如,如果数组尾部被移除(如std::vector::erase()),他们会想要移动数组的其余部分,这会发生长度为零,并且在编译时将知道该零(我看到了反汇编 - 编译器知道)但编译器仍会发出 memmove() 调用。

所以基本上我可以有一个包装器:

inline void callMemmove( void* dest, const void* source, size_t count )
{
   if( count > 0 ) {
       memmove( dest, source, count );
   }
}

但是如果count 在我不想要的编译时未知,这会引入额外的运行时检查。

是否有可能使用__assume hint 向编译器指示如果它确定count 为零,它应该消除memmove()

【问题讨论】:

  • 你希望用这个保存什么?似乎是微微优化?您要保存两个分支(查看memmove 的基本 gnu 实现)?
  • @Nim:分支,对memmove() 的调用以及(最重要的部分)允许优化memmove() 调用周围的一些代码 - 没有调用意味着它的参数准备是并不需要。是的,它是微秒,但它可以节省微秒。
  • 哦,来吧,伙计们,尖牙似乎有足够的经验知道“过早的优化是万恶之源”,除非你有严重的理由,否则你不应该实现自己的向量。在某些情况下,两者都有原因,现在让我们假设这是其中之一,让我们尝试解决问题,而不是宣布它没有问题。
  • 奇怪的是,当编译器已经检测到长度为 0 时,它应该发出对 memmove 的调用。实际上,调用应该是内联的,零大小的循环会被检测到并被忽略。为什么没有发生这种情况?您是否链接到动态运行时?如果是这样,请为 memmove 编写一个包装器,看起来就像你在上面写的那样。
  • @Konrad Rudolph:AFAIK 原因是 memmove() 是在 Visual C++ 运行时源中的汇编中实现的,而不是提供给编译器的。

标签: c++ visual-c++ optimization compiler-optimization


【解决方案1】:

__assume 的意义在于告诉编译器在优化时跳过部分代码。在您提供的链接中,示例是使用 switch 构造的 default 子句给出的 - 那里的提示告诉编译器该子句永远不会到达,即使理论上它可以。您基本上是在告诉优化器,“嘿,我知道得更好,把这段代码扔掉吧”。

对于default,你不能不写(除非你覆盖了cases中的整个范围,这有时会出现问题),因为它会导致编译错误。所以你需要提示来优化代码知道是不需要的。

在您的情况下 - 代码 可以 到达,但并非总是如此,因此 __assume 提示对您没有多大帮助。您必须检查 count 是否真的为 0。除非您确定它只能是 0,否则不要将其写入。

【讨论】:

    【解决方案2】:

    此解决方案使用C++ compile-time constant detection 中描述的技巧 - 该技巧使用编译时间整数零可以转换为指针的事实,并且可以与重载一起使用以检查“编译时间已知”属性。

    struct chkconst {
      struct Small {char a;};
      struct Big: Small {char b;};
      struct Temp { Temp( int x ) {} };
      static Small chk2( void* ) { return Small(); }
      static Big chk2( Temp  ) { return Big(); }
    };
    
    #define is_const_0(X) (sizeof(chkconst::chk2(X))<sizeof(chkconst::Big))
    #define is_const(X) is_const_0( int(X)-int(X) )
    
    #define memmove_smart(dst,src,n) do { \
        if (is_const(n)) {if (n>0) memmove(dst,src,n);} \
        else memmove(dst,src,n); \
      } while (false)
    

    或者,在您的情况下,无论如何您只想检查零,因此可以直接使用 is_const_0 以获得最大的简单性和可移植性:

    #define memmove_smart(dst,src,n) if (is_const_0(n)) {} else memmove(dst,src,n)
    

    注意:此处的代码使用的 is_const 版本比链接问题中的更简单。这是因为在这种情况下,Visual Studio 比 GCC 更符合标准。如果针对 gcc,您可以使用以下 is_const 变体(适用于处理所有可能的整数值,包括负数和 INT_MAX):

    #define is_const_0(X) (sizeof(chkconst::chk2(X))<sizeof(chkconst::Big))
    #define is_const_pos(X) is_const_0( int(X)^(int(X)&INT_MAX) )
    #define is_const(X) (is_const_pos(X)|is_const_pos(-int(X))|is_const_pos(-(int(X)+1)))
    

    【讨论】:

    • 在所有采用外部迭代器的操作中,实现不可能知道传入的迭代器不是来自同一个容器,因此不能使用memcpyerase也是如此(如果你删除了中间的一个元素,并且除此之外还有两个以上的元素,则保证范围是重叠的)。另一方面,您可以在增加缓冲区时使用memcpy,因为这样可以保证源和目标不重叠。
    • 不,这是普通的函数解析,甚至没有模板。 x(void *a) 在 value 为零常量时使用,x(Temp a) 否则使用(Temp 可以从 int 构造,但它不是零的首选重载)。我也觉得超酷。这个想法的原始来源似乎是encode.ru/threads/396-C-compile-time-constant-detection
    • @sharptooth 这不是 SFINAE,但可能会在 SFINAE 中使用。关于答案,我认为这很酷,但我真的不明白这对解决问题有何帮助。前提(据我所知)是在编译时已知为0的情况下,编译器没有删除if (count &gt; 0),如何将compile_time_constant_0 &gt; 0更改为sizeof(X) &gt; sizeof(Y)影响编译器如何生成代码? (假设在编译时 0 是已知的,那么两者都应该同样容易优化)下一个问题是Sharptooth 是如何得出这个结论的......
    • @DavidRodríguez-dribeas 不,问题是不同的 - 当使用条件时,如果 (count > 0) 和 memmove 被正确地省略为已知零,但是当值是时 if 仍然保留在那里不知道编译时间,带来不必要的开销。使用此解决方案没有开销,但消除了零移动。
    • 为什么gcc版本比较复杂?
    【解决方案3】:

    我认为您误解了__assume 的含义。当它知道值是什么时,它不会告诉编译器改变它的行为,而是告诉它当它不能自己推断时,值将是什么。

    在您的情况下,如果您告诉__assume count &gt; 0 它将跳过测试,因为您已经告诉它结果将始终为true,它将删除条件并调用@987654325 @总是,这正是你想要避免的。

    我不知道 VS 的内在函数,但在 GCC 中有一个可能/不太可能内在函数 (__builtin_expect((x),1)) 可用于提示编译器至于哪个是最可能的测试结果。这不会删除测试,但会布局代码,以便最可能(如根据您的定义)分支更有效(不会分支)。

    【讨论】:

    • VS 没有类似的可能/不太可能,这很可悲。
    • 我似乎记得默认情况下它假定第一个分支(if)被采用,这意味着如果这是最不期望的分支,您可能能够通过还原来影响生成的代码条件和 if/else 子句
    • @David Rodríguez - dribeas:当你有一个没有elseif 时,这个技巧就行不通了。
    • if (not condition) {} else { body }?或者您的意思是编译器将生成与if (condition) { body } 中相同的代码?从代码位置的角度来看,它们会生成相同的东西,代码会在if所在的位置,并且会有跳转到最后,但我不确定实际测试/跳转是否相同和/或 cpu 是否会以不同方式处理它
    【解决方案4】:

    如果可以重命名 memmove,我认为是这样的 会做 - http://codepad.org/s974Fp9k

    struct Temp {
      int x;
      Temp( int y ) { x=y; }
      operator int() { return x; };
    };
    
    void memmove1( void* dest, const void* source, void* count ) {
      printf( "void\n" );
    }
    
    void memmove1( void* dest, const void* source, Temp count ) {
      memmove( dest, source, count );
      printf( "temp\n" );
    }
    
    int main( void ) {
      int a,b;
      memmove1( &a,&b, sizeof(a) );
      memmove1( &a,&b, sizeof(a)-4 );
    }
    

    我认为如果没有课程,同样可能 - 必须查看转换规则 确认一下。

    还应该可以重载原始的 memmove(),例如。通过传递一个 对象(如 Temp(sizeof(a)) 作为第三个参数。

    不确定哪种方式更方便。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-06-02
      • 2016-12-26
      • 2010-09-25
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多