简化/优化一段查看条件组合的代码的最佳方法是什么?

时间:2014-04-18 21:04:12

标签: algorithm optimization

我有一段代码,我想优化它的可读性和性能以及酷感。现在我有这个丑陋的东西:

if      ( cond1 &&  cond2 &&  cond3 && !cond4)
{
     // do something 
}
else if ( cond1 &&  cond2 && !cond3 &&  cond4)
{
    // do something 
}
else if ( cond1 && !cond2 &&  cond3 &&  cond4)
{
   // do something 
}
else if (!cond1 &&  cond2 &&  cond3 &&  cond4)
{
    // do something 
}
else
{
    // do something 
}

其中cond1cond2cond3cond4是在上面的代码块之前已初始化的布尔值。我想让它更快,更简单,更酷。

我正在考虑这样做:

int val = (cond1 ? 0 : 1) + 2 * (cond2 ? 0 : 1) + 4 * (cond3 ? 0 : 1) + 8 * (cond4 ? 0 : 1);
if (val == 8)
{
    // do something
}
else if (val == 4)
{
    // do something 
}
else if (val == 2)
{
    // do something
}
else if (val == 1)
{
    // do something
}
else
{
    // do something 
}

这有用还是有缺陷?有没有更好的办法?在查看多个条件的不同组合时,获得所需结果的典型方法是什么?

10 个答案:

答案 0 :(得分:25)

您希望将您的值转换为位标志。也就是说,对于每个条件,您都希望在整数类型中设置或不设置。然后,您的案例中的每个4位值代表您的上述ANDed条件之一。之后,您可以使用switch语句。它可以说更具可读性,编译器通常可以将其优化为跳转表。也就是说,它只是将您的程序计数器偏移到查找表中的某个值或某种类型的值,并且您不再需要检查每个值的组合。通过这种方式,对ANDed情况的检查变为恒定时间而不是线性,即如果你添加了4个标志,现在有256个组合而不是16个组合,那么这个方法在大方式上会同样快。或者,如果您不信任编译器使switch语句成为跳转表,则可以使用flags值作为函数指针数组的索引来自行完成。它也可能值得注意的是,ORed枚举案例值在编译时被折叠或预先计算。

  enum {
    C1 = 0x1,
    C2 = 0x2,
    C3 = 0x4,
    C4 = 0x8
  };

  unsigned flags = 0;
  flags |= cond1 ? C1 : 0x0;
  flags |= cond2 ? C2 : 0x0;
  flags |= cond3 ? C3 : 0x0;
  flags |= cond4 ? C4 : 0x0;

  switch (flags) {
    case 0: // !cond1 && !cond2 && !cond3 && !cond4
      // do something
      break;
    case C1: //  cond1 && !cond2 && !cond3 && !cond4
      // do something
      break;
    case C2: // !cond1 &&  cond2 && !cond3 && !cond4
      // do something
      break;
    case C1 | C2: //  cond1 &&  cond2 && !cond3 && !cond4
      // do something
      break;
    case C3: // !cond1 && !cond2 &&  cond3 && !cond4
      // do something
      break;
    case C1 | C3: //  cond1 && !cond2 &&  cond3 && !cond4
      // do something
      break;
    case C2 | C3: // !cond1 &&  cond2 &&  cond3 && !cond4
      // do something
      break;
    case C1 | C2 | C3: //  cond1 &&  cond2 &&  cond3 && !cond4
      // do something
      break;
    case C4: // !cond1 && !cond2 && !cond3 &&  cond4
      // do something
      break;
    case C1 | C4: //  cond1 && !cond2 && !cond3 &&  cond4
      // do something
      break;
    case C2 | C4: // !cond1 &&  cond2 && !cond3 &&  cond4
      // do something
      break;
    case C1 | C2 | C4: //  cond1 &&  cond2 && !cond3 &&  cond4
      // do something
      break;
    case C3 | C4: // !cond1 && !cond2 &&  cond3 &&  cond4
      // do something
      break;
    case C1 | C3 | C4: //  cond1 && !cond2 &&  cond3 &&  cond4
      // do something
      break;
    case C2 | C3 | C4: // !cond1 &&  cond2 &&  cond3 &&  cond4
      // do something
      break;
    case C1 | C2 | C3 | C4: //  cond1 &&  cond2 &&  cond3 &&  cond4
      ; // do something
  };

此外,这涵盖了所有组合。如果您只是需要一些子集随时删除一些案例。编译器非常擅长优化switch语句。它可能比你可以自己动手的任何聪明的特殊情况算术技巧更快。

  enum {
    C1 = 0x1,
    C2 = 0x2,
    C3 = 0x4,
    C4 = 0x8
  };

  unsigned flags = 0;
  flags |= cond1 ? C1 : 0x0;
  flags |= cond2 ? C2 : 0x0;
  flags |= cond3 ? C3 : 0x0;
  flags |= cond4 ? C4 : 0x0;

  switch (flags) {
    case C1 | C2 | C3: //  cond1 &&  cond2 &&  cond3 && !cond4
      // do something
      break;
    case C1 | C2 | C4: //  cond1 &&  cond2 && !cond3 &&  cond4
      // do something
      break;
    case C1 | C3 | C4: //  cond1 && !cond2 &&  cond3 &&  cond4
      // do something
      break;
    case C2 | C3 | C4: // !cond1 &&  cond2 &&  cond3 &&  cond4
      // do something
      break;
    default:
      // do something
      ;
  };

答案 1 :(得分:17)

嗯,最愉快的写作方式可能是

if(cond1 + cond2 + cond3 + cond4 == 3)
{
    if(!cond1)
    {
        // do something
    }
    else if(!cond2)
    {
        // do something
    }
    else if(!cond3)
    {
        // do something
    }
    else // !cond4
    {
        // do something
    }
}
else
{
    // do something
}

我对那些不在数组中的值持谨慎态度。

答案 2 :(得分:5)

根据我的经验 - 有一系列长if / else语句表示类似但可区分的行为。

我通常会尝试引入一个接口实现类来捕获此行为,并调用一个可以产生所需结果的方法。

这使得代码更具可读性 - 无需重复复杂的流程(可能会在以后嵌套)。每个类负责抽象方法的实现,被操作的对象将具有接口的静态类型,以及它的动态类型 - 最匹配的实现类。 / p>

答案 3 :(得分:4)

在左边放置最有可能是假的条件,如果没有检查剩余条件就把它继续到另一个,这样你就可以获得性能,即所谓的短路

答案 4 :(得分:3)

对可读性和良好性能代码的要求可能是矛盾的,并且没有简单的“一刀切”解决方案。我的建议是明确优先考虑您希望通过重构实现的目标。什么是主要"为什么"为什么要优化代码

优化可读性

使用声明性表驱动的aproach来声明分支它们的条件和其他工件(它们做什么)。使用代码生成器从中创建丑陋的代码。表格通常很容易阅读(BTW"难看的东西"一旦你把它作为一个带有列[cond1,cond2,cond3,cond4,action]的表读取,你想要优化很容易阅读

不要打扰。无论如何,如果没有着色器或定义工具和其他代码读取增强器,今天的大多数代码($ - > _ ::。:==>等)都无法读取。例如Code Rocket可以在IDE中显示自动易读的流程图

优化性能

确切有效的优化技术取决于compiler and the target。只有通过分析生成的低级机器代码才能确定有效的方法。例如它对硬件管道有多友好。对于CISC或RICS架构,这可能有所不同。在您结合条件的情况下,尤其是branch prediction非常重要。很多时候,直接jump tables可能更有效率。真实和抽象处理器通常都有针对这种情况的特殊优化指令。另一方面,稍微不同的复制/粘贴代码可能表现更好,但可维护性较差

优化凉爽

不知道。我最近学到的酷代码定义是:

  • 正确(没有错误)
  • 便携式(在其他项目中可以轻松重复使用,最好是黑盒子。复制/粘贴友好是加号)
  • 记录(它的作用和原因 - 至少在嵌入式评论中解释)

简化/优化条件组合的典型方法

  • 目前的硬件人员经常在VHDL代码的级别上解决这个难题。他们有一些电路输出信号模拟器来帮助
  • 老硬件家伙也解决了circuit minimization,例如在我出生之前很久就已经出现了Karnaugh's maps
  • 软件测试人员在寻找具有足够代码覆盖率的测试组合时解决了这个问题。一种技术是decision tables

答案 5 :(得分:3)

如果您可以使用c ++ 11,您可以根据我之前的答案重写它:How to simplify multiple if-else-if statements in c++

switch(combine(cond1, cond2, cond3, cond4))
{
    case combine(1,1,1,0): do_something(1); break;
    case combine(1,1,0,1): do_something(2); break;
    case combine(1,0,1,1): do_something(3); break;
    case combine(0,1,1,1): do_something(4); break;
    default: 
        do_something_else();
        break;
}

它的美妙之处在于combine是完全编译时评估的 - 利用constexpr的权力。

它也是可变的(所以它最多支持编译器配置中int_max_t的位数)。

完整示例: Live On Coliru 。机制(您可以将其添加到某个标题中,例如logic_combine.hpp):

#include <iostream>
#include <iomanip>
#include <limits>
#include <cstdint>

namespace detail
{
        // a little overkill to have a functor here too, but it's a good habit™
    template <typename T = uintmax_t>
    struct to_bitmask_f
    {
        template <typename... Flags> struct result { typedef T type; };

        template <typename... Flags>
            typename result<Flags...>::type
            constexpr operator()(Flags... flags) const {
                static_assert(sizeof...(Flags) < std::numeric_limits<uintmax_t>::digits, "Too many flags for integral representation)");
                return impl(flags...);
            }

    private:
        constexpr static inline T impl() { return {}; }
        template <typename... Flags>
            constexpr static inline T impl(bool b, Flags... more) { 
            return (b?1:0) + (impl(more...) << (T(1)));
        }
    };
}

template <typename T = uintmax_t, typename... Flags>
    constexpr T combine(Flags... flags)
{
    return detail::to_bitmask_f<T>()(flags...);
}

示威:

void do_something(int i) { std::cout << "something " << i << "\n"; }
void do_something_else() { std::cout << "something else\n"; }

void f(bool cond1, bool cond2, bool cond3, bool cond4) {
    switch(combine(cond1, cond2, cond3, cond4))
    {
        case combine(1,1,1,0): do_something(1); break;
        case combine(1,1,0,1): do_something(2); break;
        case combine(1,0,1,1): do_something(3); break;
        case combine(0,1,1,1): do_something(4); break;
        default: 
            do_something_else();
            break;
    }
}

int main()
{
    // some test-cases
    f(1,0,1,0);
    f(1,0,0,1);
    f(0,1,1,0);
    f(0,1,0,1);
    f(0,1,1,1);
    f(1,1,1,1);
    f(1,1,1,0);
    f(0,0,0,0);
}

打印

something else
something else
something else
something else
something 4
something else
something 1
something else

答案 6 :(得分:2)

您建议的修改有效。当除了一个条件之外的所有条件都是真的时,你得到2的幂,并且你获得的2的幂是由哪个条件是错误决定的。通常,您可以通过这种方式处理任何简短的真/假组合列表,只需使用您希望用作测试用例的组合的二进制表示。为了便于阅读,您可能希望在if语句中以二进制而不是十进制编写组合编号,这样您就可以轻松判断要检查的是/哪些组合。

答案 7 :(得分:2)

这只是在寻找某种查找表和函数指针/委托。应该避免使用if / elseif / elseif / elseif模式,这很难理解,很难理解。

int val=....和大型假开关语句替换为enumb action = getAction(); actions(action)();。它很简单,很简单,如果出现问题,它可以让您立即关注问题所在:操作编码错误,或者您选择了错误的操作。这两种可能性都很容易调查,并且可以直接进行。

当然,根据您的语言和条件,您可能希望使用界面或子类型(多态)这样做,这自然会看起来不同,但基本上它可以归结为同样的事情:

答案 8 :(得分:1)

我怀疑四个独立的&#39;寻找布尔,其中包括16个案例,其中只有4个似乎是预期的,加上一个默认案例。真的可以涵盖任何组合吗?其中有几个是在同一个地方设置还是相互依赖?如果遇到默认情况,通常会出现什么情况?

事实上,您通常在四个而不是一个 true 值中寻找一个 false 值,这表明四个布尔值可能不是正确的数据结构。< / p>

您是否应该使用包含五个左右值的枚举,例如notCond1notCond2notCond3notCond4notAny?如果您在cond1cond2为假时执行某些特殊操作,但两者都不是,则应更改设置其中每一个的代码,以确保两者都不为假,并且根据什么是可接受的,提出错误或改变另一个错误? 除非你真的对代码的大小有所限制,否则我认为值得添加一些冗长以确保永远不会发生永远不会发生的数据状态。这提高了可读性和错误处理。

上面的代码是在循环或嵌套循环中运行,如果是,每次bool都有机会在每次循环运行时更改,还是可以将一些检查移到内部循环之外?

可能是你为每个组合调用的单独代码实际上有一些共同点可以被考虑在内。您可能希望在几个步骤中进行重构(为简单起见,仅使用两个bool):

if (cond1 && !cond2) {
    SomethingGeneral();
    Cond1Specific();
}
if (cond2 && !cond1) {
    SomethingGeneral();
    Cond2Specific();
}

if (cond1 != cond2) {
    SomethingGeneral();
}
if (cond1) {
    Cond1Specific();
}
if (cond2) {
    Cond2Specific();
}

if (MoreThanOneModeSet()) {
    break; // or throw?
}
SomethingGeneral();
if (mode == Mode1) {
    Cond1Specific();
}
if (mode == Mode2) {
    Cond2Specific();
}

上述大多数建议可能最终会引导您走向@ amit非常重要且正确的解决方案。如果你真的,真的没有看到它们中的任何一个是有效的,你应该选择@ Apriori在所有可能组合之间切换的完整和完整的方式。

如果您的代码长度和分享权限允许,您还应该尝试在codereview.stackexchange.com上发布更完整的代码段,您在那里获得的建议将有一个稍微不同的焦点,并希望是对帮助SO。

答案 9 :(得分:1)

您没有说明您的条件的复杂性。这对我来说是一个关键的决定因素。

我现在会忽略'更快'。我稍后会再回过头来看。

如果符合Servant PatternStrategy Pattern

  1. 你的if-elseif模式适用于所有条件
  2. 在条件中使用的'state'很小,或者可以重构为'state object / struct',这样可以更容易地传递给函数
  3. 条件列表比您在此处包含的内容更长和/或更复杂
  4. 代码:

    class CondState {
      int x;
      int y;
    
      // I'd use bool? or Lazy<bool> in C# but if you don't have
      // those constructs use a simple tri-state enum.
      // Unknown = 0 is not yet evaluated
      // True    = 1
      // False   = 2
      TriState cond1Cache = 0;
      TriState cond2 = 0;
      TriState cond3 = 0;
    }
    
    function Rule1(ref CondState state) : bool // returns true when done
    {
      if (... first complex condition) {
        ... do complex action
        return true;
      }
      return false;
    }
    
    function Rule2(ref CondState state) : bool
    function Rule3(ref CondState state) : bool
    ...
    function Rule40(ref CondState state) : bool // returns true when done
    
    
    // --- in your original function
    // build state object
    var state = {} 
    
    // build the list of rules: a list of function pointers (C) 
    // or function objects (js/node/python)
    var rules = [Rule1, Rule2, Rule3 ..., Rule40]
    
    foreach (r in rules) {
      if (rule(state))
        break;
    }
    

    这是“执行”规则的更清晰的代码。它使它变得简单和清洁:

    1. 懒惰评估条件
    2. 缓存条件
    3. 有不同的规则列表(罕见,但功能强大)
    4. 最后,让我回到表演。如果性能很重要,那么只有部分条件逻辑压倒了较低级别的性能问题才有意义。当然这会增加OO开销,你必须分配列表(以保存函数refs)。但这是向更大的OO模式迈出的一步:战略模式或访客模式,一旦你超越了琐碎的样本(比如超过10个复杂的条件),我认为这是解决这个问题的“酷”。