【问题标题】:Is there a simple (possibly obfuscated) math expression for the number of days in a given month?给定月份的天数是否有一个简单的(可能是混淆的)数学表达式?
【发布时间】:2020-06-24 01:17:41
【问题描述】:

用于标准库不可用的情况。假设月份是一个无符号整数。

我有兴趣查看给出正确答案的最短算术表达式,允许或禁止按位运算符和掩码,但不允许查找表。部分表达式可以保存到一个变量中,以便于展示所使用的想法。

【问题讨论】:

  • “允许或禁止按位运算符和掩码但不允许查找表”是什么意思。那么允许按位操作吗?是否允许查找表?
  • ((((M+9)%12)%5)+1)%2 + 30 处理除了二月之外的所有事情,但看到sweetness 之后,为什么还要继续?
  • 如果您关心快速、可读的代码,查找表是正确的解决方案。如果您需要缓慢、不可读的代码,那么……玩得开心。这个问题闻起来像边缘代码高尔夫。
  • saolof,@Lundin comment 是公平的。为什么不使用查找表?
  • 小型查找表在许多形式的汇编语言中并不痛苦,无论是为了维护还是为了性能。尤其是常见的嵌入式 ISA,如 ARM,其内存速度和 CPU 速度之间的差异并不像现代 x86 那样大。 (不过,在现代 x86-64 上,通过移动立即数来实现 2 位查找表是一个不错的技巧。)

标签: date assembly micro-optimization


【解决方案1】:

这是一种只使用四个简单的算术和按位运算以及一个 26 位常量的方法:

int days_in_month(unsigned m) {
    //              121110 9 8 7 6 5 4 3 2 1 0
    return 28 + ((0b11101110111110111011001100u >> m * 2u) & 0b11);
}

如果您还想处理闰年(问题中没有提及),您可以采取类似的方法,但需要更多操作和 50 位常量:

int days_in_month2(unsigned m, bool ly) {
    return 28 + ((0b11101110111110111011011111101110111110111011001100u >> (m + 12*ly) * 2u) & 0b11);
}

如果您愿意以不同的方式通过闰年,例如,设置有点像month | 16 来表示闰年,那会更有效。

我假设您将月份传递为 1 到 12,而不是 0 到 11。

测试和生成的asm可以看on godbolt

【讨论】:

  • 好方法!一个小的变化是:28 | (0b1111101011111010111111111010111111101111010011110000u >> (4*m | 2*ly) & 0b11) 在六个汇编指令中处理带有闰年的月份长度,就像在 stackoverflow.com/revisions/62547215/1 中一样,但是根据 gobolt 用 movabs 指令替换一个 LEA 指令
【解决方案2】:

变化 om @BeeOnRope 不错的答案。

#include <stdbool.h>
int DaysPerMonth(int Month, bool IsLeapYear) {
  assert(Month >= 1 && Month <= 12);
  // 0b11101110111110111011001100u
  //   3 B   B   E   E   C   C
  return (((0x3BBEECCu | (IsLeapYear << 2*2)) >> Month*2) & 3) + 28;
}

#include <stdio.h>
int main() {
  for (int ly = 0; ly <= 1; ly++) {
    for (int m = 1; m <= 12; m++) {
      printf("(%2d %2d), ", m, DaysPerMonth(m,ly));
    }
    puts("");
  }
  return 0;
}

【讨论】:

    【解决方案3】:

    为了可读性,用伪代码强力回答:

    monthlength(month,is_leapyear) :=
        oddmonth = ( month + (month >= 8) ? 1 : 0) % 2  // Or  (month ^ (month >> 3))&1 
        feb_days_offset = (month == 2) ? 2 - is_leapyear : 0
        return 30 + oddmonth - feb_days_offset
    

    where month >= 8 也可以用位移来实现,因为它只是无符号表示中的第四位,所以奇数月是(第一位)xor(第四位),可以简洁地写为(月^(月 >> 3))&1 。类似地,减去 feb 偏移量可以认为是在 2 月翻转第二位,在闰年 2 月翻转第一位。

    没有中间变量的单行:

    monthlength(month,isleapyear) := 30 + ( month + (month >= 8 ? 1 : 0)) % 2 - (month==2 ? (2 - isleapyear) : 0)
    

    或者,使用上面讨论的技巧专门使用按位算术和移位:

    monthlength(month,leapyear) := 30 ^ (month==2)<<1 ^ (month==2)&leapyear ^ (month^month>>3)&1
    

    【讨论】:

    • 使用三元可能比乘以布尔值更好。大多数编译器都会生成无分支代码,但month==2 ? 2-leapyear : 0 对人类来说看起来更清晰。您绝对不希望任何编译器实际生成布尔值 0/1 并与之相乘,尤其是在没有快速硬件乘法的嵌入式 CPU 上。 (例如,在调试模式下,或者使用糟糕的供应商提供的编译器。)
    • 同意。以伪代码形式存在主要是由于位移技巧,最终想要使事情保持一致。
    • 最终更改了给定的答案。还可以将表达式复制粘贴到更多语言。
    • 我怀疑大多数编译器会将( month + (month &gt;= 8 ? 1 : 0)) % 2 优化为(month &gt;&gt; 3) &amp; month。一方面,因为只有在值范围分析可以证明没有设置超过第 4 位的情况下,这才有效。如果您希望它可以有效地复制粘贴到常见的低级语言中,请务必将其编写为 shift。
    • 做了更多代码打高尔夫球和检查装配输出。用一个像样的 C 编译器乘以一个布尔值会编译成与零的三元相同的代码。最终得到了根本不使用加法的按位算术版本,因为无论如何一切都是关于设置最后两位。
    【解决方案4】:
    unsigned int m, leapyr, y ;
    
    //m = month range is 1 to 12
    
    //y = year range is 00 to 99
    
    leapyr = ( ( y & 0x03 ) && 1 ); //0 means leap year and 1 means Non leap year
    
    m = 30 + ( ( m  & 1 ) ^ ( 1 && ( m & 8 ) ) ) - ( ( !( m & 13 ) ) ) - ( ( !( m & 13 ) ) & leapyr );
    

    我的回答是考虑年份。如果您不想随意使用分配 leapyr 变量 0 或 1。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2012-09-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-03-23
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多