【问题标题】:Are constants optimized by clang in Objective-C?Objective-C中的clang是否优化了常量?
【发布时间】:2017-01-06 10:49:09
【问题描述】:

我以前使用预处理器宏来定义常量,然后发现它们不是一个好主意。它们位于名为Global.h 的头文件中。所以我创建了一个Global.m 文件并从 git 中忽略了它的内容:

#if DEBUG

BOOL const GodMode = YES;
BOOL const TutorialDisabled = YES;

#elif STAGING

BOOL const GodMode = YES;
BOOL const TutorialDisabled = NO;

#else

BOOL const GodMode = NO;
BOOL const TutorialDisabled = NO;

#endif

以及具有此内容的Global.h

extern BOOL const GodMode;
extern BOOL const TutorialDisabled;

STAGING 宏在项目设置的Preprocessor Macros 中定义,我的自定义Configuration 命名为StagingSTAGING=1DEBUG 宏是 Xcode 生成的默认宏,类似于 staging:DEBUG=1

有时,我想编写只为 Debug 配置执行的代码,而不需要添加新常量。

于是我开始使用:

#if DEBUG

// do something

#endif

具有这些优点/缺点:

  • 优点:

    • // do something 甚至不存在于其他配置中,因此应用程序大小不会增加,并且不会出现拼写错误或其他东西(例如添加!)的风险,这将导致它在以下情况下运行不需要
  • 缺点:

    • “不必要的编译文件”问题又回来了;每次更改运行配置时,需要重新编译使用此配置的文件,这增加了构建时间
    • 奇怪的语法风格;我不知道我是否应该增加// do something 的缩进以匹配#if 或其周围的代码,或者我是否应该像上面那样添加空行,或者它们是否应该在开头与代码在同一缩进级别的行;有类似的东西:
if (Debug) {
    // do something
}

听起来是个更好的主意,所以我将Global.h 更改为:

extern BOOL const Debug;
extern BOOL const Staging;
extern BOOL const Release;

extern BOOL const GodMode;
extern BOOL const TutorialDisabled;

Global.m

#if DEBUG

BOOL const Debug = YES;
BOOL const Staging = NO;
BOOL const Release = NO;

BOOL const GodMode = YES;
BOOL const TutorialDisabled = YES;

#elif STAGING

BOOL const Debug = NO;
BOOL const Staging = YES;
BOOL const Release = NO;

BOOL const GodMode = YES;
BOOL const TutorialDisabled = NO;

#else

BOOL const Debug = NO;
BOOL const Staging = NO;
BOOL const Release = YES;

BOOL const GodMode = NO;
BOOL const TutorialDisabled = NO;

#endif

它成功了。

由于某种原因,

static BOOL const Debug = DEBUG;
static BOOL const Staging = STAGING;
static BOOL const Release = (!DEBUG && !STAGING);

抛出错误Use of undeclared identifier 'STAGING',即使它的定义与DEBUG 完全相同:这意味着其他配置没有DEBUG=0

//:configuration = Debug
GCC_PREPROCESSOR_DEFINITIONS = DEBUG=1

//:configuration = Staging
GCC_PREPROCESSOR_DEFINITIONS = STAGING=1

//:configuration = Release

但是,查看 Preprocess 助理编辑器视图,if (Debug) { 出现了。

我的问题是,if 检查实际上是否会保留在例如发布配置的代码中?如果是,有没有更好的方法来实现我之前描述的?

【问题讨论】:

    标签: objective-c constants


    【解决方案1】:

    没有比我们自己试一试更好的判断方法了。拿着你的Globals.hGlobals.m 文件,我写了以下main.m

    #import "Globals.h"
    #import <Foundation/Foundation.h>
    
    int main()
    {
        @autoreleasepool {
            if (Debug) {
                puts("We're in debug mode");
            } else if (Staging) {
                puts("We're in staging mode");
            } else {
                puts("We're in release mode");
            }
        }
    }
    

    很简单,只是打印出我们所处的模式。让我们编译并运行它看看会发生什么:

    $ clang -g -fobjc-arc -o var_test main.m Globals.m && ./var_test
    We're in release mode
    

    看起来很合理——我们没有提供DEBUGSTAGING,所以我们必须处于发布模式。 (-g 告诉 clang 输出调试信息,以防万一有用;-fobjc-arc 打开 ARC;-o 告诉 clang 输出具有特定名称的可执行文件。)只是为了测试:

    $ clang -g -DDEBUG -fobjc-arc -o var_test main.m Globals.m && ./var_test
    We're in debug mode
    $ clang -g -DSTAGING -fobjc-arc -o var_test main.m Globals.m && ./var_test
    We're in staging mode
    

    一切都好,看起来它有效! (-DDEBUG 在预处理器中定义了DEBUG-DSTAGINGSTAGING 也是如此。)现在的问题是,clang 是否摆脱了 if 语句并仅用 print 语句替换代码?我不会假设你的汇编技能,所以让我们看看编译器通过Hopper的伪代码模式生成了什么(这会加载编译器生成的目标代码并将其反向反汇编成类似的东西) C代码):

    int _main() {
        var_10 = objc_autoreleasePoolPush();
        if (*(int8_t *)_Debug != 0x0) {
                puts("We're in debug mode");
        }
        else {
                if (*(int8_t *)_Staging != 0x0) {
                        puts("We're in staging mode");
                }
                else {
                        puts("We're in release mode");
                }
        }
        objc_autoreleasePoolPop(var_10);
        rax = 0x0;
        return rax;
    }
    

    变量名不见了,这看起来有点像一些奇怪的 C/汇编混合,但很明显 if 语句仍然在那里......

    嗯,好吧,也许我们尝试优化一下构建?也许 Clang 没有在此处进行任何优化。

    $ clang -g -O2 -fobjc-arc -o var_test main.m Globals.m
    

    -O2 开启了一些激进的编译器优化。)料斗输出:

    int _main() {
        rbx = objc_autoreleasePoolPush();
        if (*(int8_t *)_Debug != 0x0) {
                rdi = "We're in debug mode";
        }
        else {
                if (*(int8_t *)_Staging != 0x0) {
                        rdi = "We're in staging mode";
                }
                else {
                        rdi = "We're in release mode";
                }
        }
        puts(rdi);
        objc_autoreleasePoolPop(rbx);
        return 0x0;
    }
    

    好的,略有不同,但它仍然存在!什么给了?

    对于许多编译语言,包括 C/Objective-C,编译发生在几个阶段。您在项目中包含的每个 .m 文件都被分别编译为目标代码(编译阶段),然后所有生成的目标代码文件链接在一起(链接阶段)。这里的关键是每个 .m 文件都被编译单独 - 即使变量是在Globals.m 中静态定义的,当编译main.m 时,编译器不知道@987654344 的值@ 和Staging — 它看到的只是Globals.h 中的声明,上面写着“嘿,我保证,下线有人将为这些变量提供实际值”;由链接器将它们粘合在一起。 (这就是为什么您可以将main.m 编译成main.o 就好了,即使您忘记定义DebugStaging — 只有当您尝试链接所有.o将文件放入可执行文件中,您会收到 linker 错误,告诉您存在问题。)

    好的,所以这是一个链接问题,而不是编译问题。有什么方法可以让链接器为我们解决这些问题?是的,是的。 :)

    $ clang -g -flto -fobjc-arc -o var_test main.m Globals.m && ./var_test
    We're in release mode
    

    料斗输出:

    int _main() {
        rbx = objc_autoreleasePoolPush();
        puts("We're in release mode");
        objc_autoreleasePoolPop(rbx);
        return 0x0;
    }
    

    啊,好多了! -flto 标志启用Link Time Optimization,它允许链接器在将它们粘合在一起时查看.o 文件并根据它看到的内容进行优化。启用优化后链接将花费更长的时间(您可以在 Xcode 中目标的构建设置中启用 LTO),但会执行这样的优化。


    现在,真正的问题是:这甚至是一个好方法吗?嗯,也许不是。

    仅仅为此打开 LTO 有点过头了。在您的代码中使用#if DEBUG 并没有什么坏处,而且它实际上比较常见。您可以保持缩进与其他情况完全相同:

    int main()
    {
        int x;
        if (/* some calculation setting x */) {
    #if DEBUG
            NSLog("Calculation succeeded: %d", x);
    #endif
    
            // ...
        }
    }
    

    或者,如果您希望坚持使用变量方法,欢迎您在 Globals.h 中将变量定义为 static 变量,从而完全消除 Globals.m(并摆脱对 LTO 的需求) :

    #if DEBUG
    static BOOL const Debug = YES;
    static BOOL const Staging = NO;
    static BOOL const Release = NO;
    
    static BOOL const GodMode = YES;
    static BOOL const TutorialDisabled = YES;
    #elif STAGING
    static BOOL const Debug = NO;
    static BOOL const Staging = YES;
    static BOOL const Release = NO;
    
    static BOOL const GodMode = YES;
    static BOOL const TutorialDisabled = NO;
    #else
    static BOOL const Debug = NO;
    static BOOL const Staging = NO;
    static BOOL const Release = YES;
    
    static BOOL const GodMode = NO;
    static BOOL const TutorialDisabled = NO;
    #endif
    

    您也可以使用您的

    static BOOL const Debug = DEBUG;
    static BOOL const Staging = STAGING;
    static BOOL const Release = (!DEBUG && !STAGING);
    

    版本。它不起作用的原因正是您自己所说的:“其他配置没有 DEBUG=0”。如果你不定义DEBUG,编译器应该用static BOOL const Debug = DEBUG;做什么?您应该在暂存和发布模式下定义DEBUG=0,在调试和发布模式下定义STAGING=0,这样才能正常工作。

    希望这会有所帮助!

    【讨论】:

    • 我不敢相信这个答案有多棒!从中学到了很多。我忘了说Global.m 文件还有另一个目的:摆脱只切换GodMode 之类的东西的提交的需要,这就是它在git 中被忽略的原因。
    • 将常量定义为static 可以在不激活LTO 的情况下删除未使用的#ifs?
    • 关于最后一部分,我认为static BOOL const Debug = DEBUG; 工作的原因是因为我正在使用调试配置构建项目,我很傻。所以很可能在 Staging 中运行它也会为Debug 抛出同样的错误。
    • @IulianOnofrei 很高兴能帮上忙。 :) 关于您的最后一条评论,这是正确的——您同样会得到一个错误。关于 Globals.m 没有被 git 跟踪,但是:你是唯一一个在这个项目上工作的人吗?因为如果 git 不跟踪 Globals.m,任何克隆你的 repo 的人(包括你自己)都会在编译时遇到链接器错误,因为 Globals.m 会丢失,并且没有任何东西可以定义这些变量......
    • 我不是唯一一个,但我知道这一点。我创建了一个sample.Global.m 文件和一个用正确名称复制它的小脚本:D
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-12-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多