Повышение удобочитаемости кода без снижения производительности (для этого фрагмента в C)

#c #performance #gcc #readability

#c #Производительность #gcc #удобочитаемость

Вопрос:

В довольно большой и сложной программе на C, где время выполнения является первоочередным приоритетом, я должен решить, как мне следует писать такие фрагменты кода:

 for (int i=0; i < md->global_grid[ic[0]][ic[1]][ic[2]].parts_num; i  )
{
    if (md->global_grid[ic[0]][ic[1]][ic[2]].parts[i].core.GroupID == RESERVED_GROUP)
    {
        md->global_grid[ic[0]][ic[1]][ic[2]].parts[i].core.GroupID = GroupID;
        fmd_real_t mass = md->potsys.atomkinds[md->global_grid[ic[0]][ic[1]][ic[2]].parts[i].core.atomkind].mass;
        for (int d=0; d<3; d  )
            md->global_grid[ic[0]][ic[1]][ic[2]].parts[i].core.v[d] -= MomentumSum[d] / (AtomsNum * mass);
    }
}
  

Это можно сделать более читаемым и компактным, используя указатели, подобные pc приведенным ниже:

 for (int i=0; i < md->global_grid[ic[0]][ic[1]][ic[2]].parts_num; i  )
{
    if (md->global_grid[ic[0]][ic[1]][ic[2]].parts[i].core.GroupID == RESERVED_GROUP)
    {
        particle_core_t *pc = amp;md->global_grid[ic[0]][ic[1]][ic[2]].parts[i].core;

        pc->GroupID = GroupID;
        fmd_real_t mass = md->potsys.atomkinds[pc->atomkind].mass;
        for (int d=0; d<3; d  )
            pc->v[d] -= MomentumSum[d] / (AtomsNum * mass);
    }
}
  

Но разве разыменование pc не требует некоторого процессорного времени? Обычно я использую первую форму, а иногда и вторую форму, но не знаю, какая из них действительно лучше. Я использую -O3 gcc для оптимизации.

Я знаю, что измерение времени выполнения и их сравнение может дать ответ, но знание того, что думают опытные и профессиональные программисты, всегда очень полезно. В частности, простое сравнение времени не говорит о том, почему одна форма быстрее.

Комментарии:

1. Разыменование pc может занять процессорное время, но тогда это так md->global_grid[ic[0]][ic[1]][ic[2]].parts[i].core … Вы пробовали просматривать сгенерированную сборку, чтобы узнать, одинакова она или отличается?

2. @jtbandes, в первом случае я полностью полагался на компилятор для оптимизации. И нет, я не видел сгенерированную сборку. Я не очень разбираюсь в сборке.

3. Возможно, это поможет вам начать работу: godbolt.org/z/aTrWMh

4. Во-первых, вы не доказали, что вторая форма на самом деле медленнее. Но разве разыменование PC не требует некоторого процессорного времени? Обычно я использую первую форму, а иногда и вторую форму, но не знаю, какая из них действительно лучше. Если вы не можете сказать, это не имеет значения. ВСЕГДА пишите читаемый код. Решайте проблемы с производительностью, когда они становятся очевидными и вызывают реальные проблемы . Ухудшение удобочитаемости только для того, чтобы вы могли удалить 50 миллисекунд из процесса, который занимает три часа, хуже, чем пустая трата времени — в буквальном смысле. Это значительно повышает вероятность ошибок и затрудняет их обнаружение и исправление.

5. @AndrewHenle, я думаю, вы правы, хотя в этом случае процесс может занять около недели. Большое спасибо. Приятно видеть людей, которые любят помогать друг другу. 🙂

Ответ №1:

Взгляните на сборку в примере godbolt от jtbandes. Вот сборка gcc x86-64 для читаемой версии внутреннего цикла:

 particle_core_t *pc = amp;md->global_grid[ic[0]][ic[1]][ic[2]].parts[i].core;

pc->GroupID = GroupID;
fmd_real_t mass = md->potsys.atomkinds[pc->atomkind].mass;
for (int d=0; d<3; d  )
  pc->v[d] -= MomentumSum[d] / (AtomsNum * mass);
  

gcc достаточно умен, чтобы видеть, что цикл повторяется d 3 итерации, поэтому он разворачивает его. Он также видит, что lhs на каждой итерации находится в одном и том же массиве, поэтому он эффективно сохраняет адрес массива rcx вместо повторного разыменования pc->v .

 mov     rcx, QWORD PTR [rax 8]        ; rcx = pc->v.
mov     DWORD PTR [rax], ebp
pxor    xmm0, xmm0
add     rdx, 1
movsx   rax, DWORD PTR [rax 4]
mov     rsi, QWORD PTR [r11 8]
cvtsi2sd        xmm0, r8d
movsd   xmm2, QWORD PTR [rbx]         ; Load xmm2 = MomentumSum[0].
movsd   xmm1, QWORD PTR [rcx]         ; Load xmm1 = pc->v[0].
lea     rax, [rax rax*2]
lea     rax, [rsi rax*8]
mulsd   xmm0, QWORD PTR [rax 16]      ; Compute xmm0 = AtomsNum * mass.
movsx   rax, DWORD PTR [r10]
mov     rax, QWORD PTR [r12 rax*8]
divsd   xmm2, xmm0                    ; xmm2 /= xmm0
subsd   xmm1, xmm2                    ; xmm1 -= xmm2
movsd   QWORD PTR [rcx], xmm1         ; Store pc->v[0] = xmm1.
movsd   xmm2, QWORD PTR [rbx 8]
movsd   xmm1, QWORD PTR [rcx 8]
divsd   xmm2, xmm0
subsd   xmm1, xmm2
movsd   QWORD PTR [rcx 8], xmm1       ; Store pc->v[1] = xmm1.
movsd   xmm1, QWORD PTR [rbx 16]
divsd   xmm1, xmm0
movsd   xmm0, QWORD PTR [rcx 16]
subsd   xmm0, xmm1
movsd   QWORD PTR [rcx 16], xmm0      ; Store pc->v[2] = xmm1.
movsx   rcx, DWORD PTR [r10 4]
mov     r9, QWORD PTR [rax rcx*8]
movsx   rcx, DWORD PTR [r10 8]
lea     rax, [rcx rcx*2]
lea     rsi, [r9 rax*8]
mov     edi, DWORD PTR [rsi]