问题 std :: vector 手动复制,而不是在启用C ++ 11/14时调用memcpy


使用gcc 4.9,使用Linaro工具链对ARM进行交叉编译,我找到了编译结果 vector.assign() 添加时更改 -std=c++14,以一种造成重大性能问题的方式。

我已经尝试了几种不同的方式来执行此分配+复制,但只要我使用它们,所有这些方法都会遇到此问题 std::vector 去做吧。

我可以通过这个玩具示例重现问题:

VectorTest.h

#include <stdint.h>
#include <stddef.h>
#include <vector>

struct VectorWrapper_t
{
    VectorWrapper_t(uint8_t const* pData, size_t length);
    std::vector<uint8_t> data;
};

VectorTest.cpp

#include "VectorTest.h"

VectorWrapper_t::VectorWrapper_t(uint8_t const* pData, size_t length)
{
    data.assign(pData, pData + length);
}

gcc标志:

-std=c++14 \
-mthumb -march=armv7-a -mtune=cortex-a9 \
-mlittle-endian -mfloat-abi=hard -mfpu=neon -Wa,-mimplicit-it=thumb \
-O2 -g

查看程序集,我可以看到原因:原始版本(C ++ 03,我假设?)调用 memmove而C ++ 14版本则添加了一个额外的循环,它看起来像是手动复制数据。看着 .loc 标签gcc添加 -fverbose-asm,这个循环中的指令来自 stl_construct.h 和 stl_uninitialized.h

更改为gcc 5.2.1(使用C ++ 14),它的编译几乎与C ++ 03示例完全相同,除了 memcpy 代替 memmove

我可以通过使用来解决这个问题 std::unique_ptr<uint8_t[]> 代替 vector 这里。但是,我想了解这个问题的底部,以确定是否有其他地方使用 vectors可能有性能问题以及如何解决它们(更新到gcc 5.2是不实际的)。

所以我的问题是: 为什么它在C ++ 11/14下的编译方式不同?

以供参考, gcc --version 报告:
arm-linux-gnueabihf-gcc (Linaro GCC 4.9-2014.12) 4.9.3 20141205 (prerelease)

这是生成的程序集gcc:

# C++03, gcc 4.9

    push    {r3, r4, r5, r6, r7, lr}    @
    movs    r3, #0  @ tmp118,
    mov r4, r0  @ this, this
    str r3, [r0]    @ tmp118, MEM[(struct _Vector_impl *)this_1(D)]._M_start
    mov r5, r2  @ length, length
    str r3, [r0, #4]    @ tmp118, MEM[(struct _Vector_impl *)this_1(D)]._M_finish
    str r3, [r0, #8]    @ tmp118, MEM[(struct _Vector_impl *)this_1(D)]._M_end_of_storage
    cbnz    r2, .L19    @ length,
    mov r0, r4  @, this
    pop {r3, r4, r5, r6, r7, pc}    @
.L19:
    mov r0, r2  @, length
    mov r6, r1  @ pData, pData
    bl  _Znwj   @
    mov r2, r5  @, length
    mov r1, r6  @, pData
    mov r7, r0  @ D.13516,
    bl  memmove @
    ldr r0, [r4]    @ D.13515, MEM[(struct vector *)this_1(D)].D.11902._M_impl._M_start
    cbz r0, .L3 @ D.13515,
    bl  _ZdlPv  @
.L3:
    add r5, r5, r7  @ D.13515, D.13516
    str r7, [r4]    @ D.13516, MEM[(struct vector *)this_1(D)].D.11902._M_impl._M_start
    str r5, [r4, #4]    @ D.13515, MEM[(struct vector *)this_1(D)].D.11902._M_impl._M_finish
    mov r0, r4  @, this
    str r5, [r4, #8]    @ D.13515, MEM[(struct vector *)this_1(D)].D.11902._M_impl._M_end_of_storage
    pop {r3, r4, r5, r6, r7, pc}    @
.L6:
    ldr r0, [r4]    @ D.13515, MEM[(struct _Vector_base *)this_1(D)]._M_impl._M_start
    cbz r0, .L5 @ D.13515,
    bl  _ZdlPv  @
.L5:
    bl  __cxa_end_cleanup   @

# C++14, gcc 4.9

    push    {r3, r4, r5, r6, r7, lr}    @
    movs    r3, #0  @ tmp157,
    mov r6, r0  @ this, this
    str r3, [r0]    @ tmp157, MEM[(struct _Vector_impl *)this_1(D)]._M_start
    mov r5, r2  @ length, length
    str r3, [r0, #4]    @ tmp157, MEM[(struct _Vector_impl *)this_1(D)]._M_finish
    str r3, [r0, #8]    @ tmp157, MEM[(struct _Vector_impl *)this_1(D)]._M_end_of_storage
    cbnz    r2, .L25    @ length,
    mov r0, r6  @, this
    pop {r3, r4, r5, r6, r7, pc}    @
.L25:
    mov r0, r2  @, length
    mov r4, r1  @ pData, pData
    bl  _Znwj   @
    adds    r3, r4, r5  @ D.20345, pData, length
    mov r7, r0  @ __result,
    cmp r4, r3  @ pData, D.20345
    ittt    ne
    addne   r1, r4, #-1 @ ivtmp.76, pData,
    movne   r3, r0  @ __result, __result
    addne   r4, r0, r5  @ D.20346, __result, length
    beq .L26    @,
.L7:
    ldrb    r2, [r1, #1]!   @ zero_extendqisi2  @ D.20348, MEM[base: _48, offset: 0]
    cbz r3, .L6 @ __result,
    strb    r2, [r3]    @ D.20348, MEM[base: __result_23, offset: 0B]
.L6:
    adds    r3, r3, #1  @ __result, __result,
    cmp r3, r4  @ __result, D.20346
    bne .L7 @,
.L8:
    ldr r0, [r6]    @ D.20346, MEM[(struct vector *)this_1(D)].D.18218._M_impl._M_start
    cbz r0, .L5 @ D.20346,
    bl  _ZdlPv  @
.L5:
    str r7, [r6]    @ __result, MEM[(struct vector *)this_1(D)].D.18218._M_impl._M_start
    mov r0, r6  @, this
    str r4, [r6, #4]    @ D.20346, MEM[(struct vector *)this_1(D)].D.18218._M_impl._M_finish
    str r4, [r6, #8]    @ D.20346, MEM[(struct vector *)this_1(D)].D.18218._M_impl._M_end_of_storage
    pop {r3, r4, r5, r6, r7, pc}    @
.L26:
    adds    r4, r0, r5  @ D.20346, __result, length
    b   .L8 @
.L11:
    ldr r0, [r6]    @ D.20346, MEM[(struct _Vector_base *)this_1(D)]._M_impl._M_start
    cbz r0, .L10    @ D.20346,
    bl  _ZdlPv  @
.L10:
    bl  __cxa_end_cleanup   @

# C++14, gcc 5.2

    push    {r3, r4, r5, r6, r7, lr}    @
    movs    r3, #0  @ tmp118,
    mov r4, r0  @ this, this
    str r3, [r0]    @ tmp118, MEM[(struct _Vector_impl *)this_1(D)]._M_start
    str r3, [r0, #4]    @ tmp118, MEM[(struct _Vector_impl *)this_1(D)]._M_finish
    str r3, [r0, #8]    @ tmp118, MEM[(struct _Vector_impl *)this_1(D)]._M_end_of_storage
    cbnz    r2, .L19    @ length,
    mov r0, r4  @, this
    pop {r3, r4, r5, r6, r7, pc}    @
.L19:
    mov r0, r2  @, length
    mov r6, r1  @ pData, pData
    mov r5, r2  @ length, length
    bl  _Znwj   @
    mov r2, r5  @, length
    mov r1, r6  @, pData
    mov r7, r0  @ D.20824,
    bl  memcpy  @
    ldr r0, [r4]    @ D.20823, MEM[(struct vector *)this_1(D)].D.18751._M_impl._M_start
    cbz r0, .L3 @ D.20823,
    bl  _ZdlPv  @
.L3:
    add r5, r5, r7  @ D.20823, D.20824
    str r7, [r4]    @ D.20824, MEM[(struct vector *)this_1(D)].D.18751._M_impl._M_start
    str r5, [r4, #4]    @ D.20823, MEM[(struct vector *)this_1(D)].D.18751._M_impl._M_finish
    mov r0, r4  @, this
    str r5, [r4, #8]    @ D.20823, MEM[(struct vector *)this_1(D)].D.18751._M_impl._M_end_of_storage
    pop {r3, r4, r5, r6, r7, pc}    @
.L6:
    ldr r0, [r4]    @ D.20823, MEM[(struct _Vector_base *)this_1(D)]._M_impl._M_start
    cbz r0, .L5 @ D.20823,
    bl  _ZdlPv  @
.L5:
    bl  __cxa_end_cleanup   @

3974
2018-02-01 21:15


起源

它可能不会产生太大的影响,但为什么要分配而不是初始化?后者可以避免一次分配。 VectorWrapper_t(uint8_t const* pData, size_t length) : data(pData, pData + length) {} - juanchopanza
这个问题似乎更适合Linaro错误报告。 - ildjarn
2种可能性:要么代码开始相同,要么gcc-5 +检测到循环等同于memcpy并用memcpy调用替换它,或者libstdc ++获得了一些调用memcpy的新特殊路径。尝试传球 -fdump-tree-all 并查看一些早期/晚期转储以确定发生了什么。 - Marc Glisse
我手边没有任何标准,但它们之间是否有指定变更的规范?如果没有,我称之为回归错误 - MikeMB
@AndreiR。该 move 在 memmove 与C ++ 11移动语义无关。两个函数都没有修改源数组。 - Barry


答案:


这是4.9.2版本中的GCC错误,请参阅 PR 64476。默认区别 -std=gnu++03 模式和 -std=c++14 对于C ++ 11及更高版本而言,它可能具有不可分配的普通类型(因为它们可以具有已删除的赋值运算符),这会导致实现 std::uninitialized_copy 采用不同的(较慢的)代码路径。对可分配性的检查是错误的,这意味着当我们不需要时,我们采取了缓慢的路径。

两年前我为GCC 4.9.3修复了它,但是你的编译器是基于4.9.2和4.9.3版本之间的快照,并且有几周的时间来修复它。

你可以要求Linaro将他们的GCC 4.9编译器更新到4.9.4,或者至少应用补丁来修复这个bug。


11
2018-02-07 11:29