【问题标题】:How to use VC++ intrinsic functions w/o run-time library如何使用没有运行时库的 VC++ 内部函数
【发布时间】:2011-02-25 16:58:54
【问题描述】:

我参与了您尝试生成尽可能小的二进制文件的挑战之一,因此我正在构建我的程序没有 C 或 C++ 运行时库 (RTL)。我没有链接到 DLL 版本或静态版本。我什至没有#include 头文件。我有这个工作正常。

一些 RTL 函数,例如 memset(),可能很有用,所以我尝试添加自己的实现。它在调试版本中运行良好(即使对于那些编译器生成对memset()隐式 调用的地方)。但是在发布版本中,我收到一条错误消息,说我无法定义内在函数。你看,在 Release 版本中,内部函数是启用的,memset() 是一个内部函数。

我很想在我的发布版本中使用 memset() 的内在函数,因为它可能是内联的,并且比我的实现更小、更快。但我似乎是第 22 条军规中的一员。如果我没有定义memset(),链接器会抱怨它未定义。如果我定义了它,编译器会抱怨我无法定义一个内在函数。

有谁知道定义、声明、#pragma 以及编译器和链接器标志的正确组合,以便在不增加 RTL 开销的情况下获得内在函数?

Visual Studio 2008、x86、Windows XP+。

让问题更具体一点:

extern "C" void * __cdecl memset(void *, int, size_t);

#ifdef IMPLEMENT_MEMSET
void * __cdecl memset(void *pTarget, int value, size_t cbTarget) {
    char *p = reinterpret_cast<char *>(pTarget);
    while (cbTarget > 0) {
        *p++ = static_cast<char>(value);
        --cbTarget;
    }
    return pTarget;
}
#endif

struct MyStruct {
    int foo[10];
    int bar;
};

int main() {
    MyStruct blah;
    memset(&blah, 0, sizeof(blah));
    return blah.bar;
}

我是这样构建的:

cl /c /W4 /WX /GL /Ob2 /Oi /Oy /Gs- /GF /Gy intrinsic.cpp
link /SUBSYSTEM:CONSOLE /LTCG /DEBUG /NODEFAULTLIB /ENTRY:main intrinsic.obj

如果我使用 memset() 的实现进行编译,我会收到编译器错误:

error C2169: 'memset' : intrinsic function, cannot be defined

如果我在没有实现 memset() 的情况下编译它,我会收到链接器错误:

error LNK2001: unresolved external symbol _memset

【问题讨论】:

  • 问题出在/GL,请看下面我的回答。

标签: c++ visual-c++ intrinsics memset demoscene


【解决方案1】:

我想我终于找到了解决办法:

首先,在头文件中,使用 pragma 声明 memset(),如下所示:

extern "C" void * __cdecl memset(void *, int, size_t);
#pragma intrinsic(memset)

这允许您的代码调用memset()。大多数情况下,编译器会内联内部版本。

其次,在一个单独的实现文件中,提供一个实现。防止编译器抱怨重新定义内部函数的技巧是首先使用另一个 pragma。像这样:

#pragma function(memset)
void * __cdecl memset(void *pTarget, int value, size_t cbTarget) {
    unsigned char *p = static_cast<unsigned char *>(pTarget);
    while (cbTarget-- > 0) {
        *p++ = static_cast<unsigned char>(value);
    }
    return pTarget;
}

这为优化器决定不使用内部版本的情况提供了一种实现。

突出的缺点是您必须禁用整个程序优化(/GL 和 /LTCG)。我不确定为什么。如果有人找到一种方法可以在不禁用全局优化的情况下做到这一点,请加入。

【讨论】:

  • 所有演员都在那里做什么?此外,与void * 之间的指针转换通常是static_cast-s,而不是reinterpret_cast-s。
  • @AndreyT:我已将演员阵容从void * 改为使用static_cast。在我最初写这篇文章的时候,在那种情况下使用哪个演员表还不清楚并且引起了激烈的争论。 (stackoverflow.com/questions/310451/…) 我不确定你对“所有”这些案例的意思。那里有两个。第一个是必要的,因为您不能通过指向 void 的指针进行写入(这是 memset 所采用的)。第二个是编译器不会警告将 int 分配给 unsigned char。
  • 您可以通过将这些内在函数编译到单独的静态库中来将整个程序优化的禁用限制为仅限于内在函数。
  • @KeyC0de:不完全是。我创建了一个常规函数并告诉编译器在它会使用相应内在函数的任何地方使用它。这允许我在没有编译器的私有运行时库的情况下进行链接,从而让我保持可执行文件很小。有可能(很可能)真正的内在函数会更好地优化速度,但更小的尺寸对我来说更重要。
  • 所以如果我得到你,你的行为与我所说的相反。 memset 是 MSVS 编译器中的一个内在函数(对吗?),而你是非内在的?那是因为内在函数会消耗对您很重要的二进制文件的空间。
【解决方案2】:
  1. 我很确定有一个编译器标志告诉 VC++ 不要使用内在函数

  2. 运行时库的源代码随编译器一起安装。您确实可以选择想要/需要的摘录功能,但通常您必须对它们进行大量修改(因为它们包含您不想要/不需要的功能和/或依赖项)。

  3. 还有其他可用的开源运行时库,可能需要较少的自定义。

  4. 如果您对此非常认真,则需要了解(并且可能会使用)汇编语言。

编辑添加:

我得到了你的新测试代码来编译和链接。这些是相关设置:

Enable Intrinsic Functions: No
Whole Program Optimization: No

最后一个抑制“编译器助手”,如内置 memset。

编辑添加:

现在它已解耦,您可以将 asm 代码从 memset.asm 复制到您的程序中——它有一个全局引用,但您可以删除它。它足够大,可以内联,但如果你去掉它用来提高速度的所有技巧,你也许可以让它足够小。

我拿了你上面的例子,用这个替换了memset()

void * __cdecl memset(void *pTarget, char value, size_t cbTarget) {
    _asm {
    push ecx
    push edi

    mov al, value
    mov ecx, cbTarget
    mov edi, pTarget
    rep stosb

    pop edi
    pop ecx
    }
    return pTarget;
}

它可以工作,但库的版本要快得多。

【讨论】:

  • 但这与尝试制作尽可能小的二进制文件的最终目标背道而驰。在很多情况下,包括memset,内联的内在函数比函数调用要小。
  • lib 版本更快,因为它将目标指针对齐到 4 个字节(在 32 位机器中,在 64 位中为 8 个字节)并使用 rep stosd 而不是 rep stosb,将未对齐的字节单独写入开始和结束。这样做会使 memset 更大。再一次(正如我在 cmets 中对我的回答所说),我认为您的编译器并没有真正生成内在函数。 Egrunin 的实现尽可能小。在非常特殊的情况下,如果 ecx&edi 可用,内在函数可能能够节省推送/弹出。你会有净收益吗?很少,我猜。
  • egrunin第二次编辑的代码与编译器使用intrinsic时生成的代码基本相同。当编译器知道不需要保留 ecx 和 edi 时,它通常能够保存几个字节。当要清除的字节数变大时,库版本会得到回报。处理可能未对齐的开头和结尾会产生开销。
  • @Adrian:所以...我回答你的问题了吗?
  • 你写的一切都是真的,但它并没有真正解决我的问题。这可能是我的错,因为问题不够清楚。关闭优化与保持程序小(这就是我首先尝试省略 RTL 的原因)和快速(这是次要目标)背道而驰。当它与编译器生成的几乎相同时,似乎不需要将程序集插入我的代码中。感谢您的意见。
【解决方案3】:

我认为您必须将 Optimization 设置为“Minimize Size (/O1)”或“Disabled (/Od)”才能编译 Release 配置;至少这就是我使用 VS 2005 的诀窍。内在函数是为速度而设计的,因此可以为其他优化级别(速度和完整)启用它们是有道理的。

【讨论】:

  • 我已经有了 /O1,而 /Od 有点违背了制作尽可能小的二进制文件的目标。速度也是一个问题。
  • 好吧,我面前没有 VS2008,所以也许他们改变了一些东西。在 VS2005 中,这是我必须进行的唯一更改才能使其成功构建。
【解决方案4】:

这绝对适用于 VS 2015: 添加命令行选项 /Oi-。这是有效的,因为内在函数上的“否”不是开关,它是未指定的。 /Oi- 并且您的所有问题都消失了(它应该适用于整个程序优化,但我还没有正确测试过)。

【讨论】:

  • 来自 MSDN:“/Oi 只是对编译器的请求,用内在函数替换一些函数调用;编译器可能会调用该函数(而不是用内在函数替换函数调用),如果它会导致在更好的表现。”所以它可能适用于所有情况,也可能不适用。
【解决方案5】:

当您第一次提出问题时,这当然不是答案,但现在可以通过使用 Visual Studio 2019 提供的 Clang 版本来做您想做的事情,它可以在没有您想要的情况下工作任何特定的箍要跳过。

使用 Clang 也有其他一些好处 - 特别是如果您也希望使用 x64 架构实现类似的目标,因为它似乎是让被破坏的 pdata 部分消失的唯一方法!

根据 Visual C++ 本身,我采取了将 memset/memcpy 的实现放在单独的源文件中的方法,并且正如 rc-1290 提到的那样,仅从全局优化中排除了一个文件,因此成本并不高 -虽然很烦人!

【讨论】:

    【解决方案6】:

    只需将函数命名为稍有不同。

    【讨论】:

    • 好主意,但它不起作用。我编写了自己的版本,称为ClearMemory(),使用命名空间以确保它不会与其他任何内容发生冲突。优化器用调用memset()(字节值为0)替换了我对ClearMemory() 的实现!为了自己的利益太聪明了。 :-)
    • 如果编译器首先使用memset(例如在类初始化程序中),这也不起作用。
    • 在您想要写入零的特定情况下,SecureZeroMemory 函数似乎可以工作。 (它被实现为嵌入到 winnt.h 中的强制内联函数。)
    【解决方案7】:

    “常规”运行时库执行此操作的方式是编译具有 memset 定义的程序集文件并将其链接到运行时库(您可以在 C:\Program Files\Microsoft Visual Studio 中或周围找到程序集文件10.0\VC\crt\src\intel\memset.asm)。即使在整个程序优化的情况下,这种事情也能正常工作。

    还要注意,编译器只会在某些特殊情况下使用 memset 内在函数(当大小恒定且很小时?)。它通常会使用你提供的 memset 函数,所以你应该使用 memset.asm 中的优化函数,除非你要写一些优化的东西。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-12-31
      • 1970-01-01
      • 2022-06-10
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多