Perfect for loop

Today, the format of an article is unusual for me: I rather ask the audience a question than share a ready-made recipe. However, I also propose a recipe to initiate a discussion. So today we are going to talk about the feeling of beauty.



I've been writing code for quite some time, and it just so happens that it's almost always in C ++. I can't even estimate how many times I've written such a construction:



for (int i=0; i<size; i++) {
    [...]
}


Although why I can't, I really can:



find . \( -name \*.h -o -name \*.cpp \) -exec grep -H "for (" {} \; | wc -l
43641


Our current project contains 43 thousand cycles. I'm not the only one who saws the project, but the team is small and my project is not the first (and, I hope, not the last), so it will be used as a rough estimate. How forgood is this loop recording ? Indeed, in fact, it is not even important the number of times when I wrote the cycle , but the number of times when I read the cycle (see debugging and code review). And here we are obviously talking about millions.



At KPDV, a node is called the "perfection loop" .



image



So what is the perfect cycle?



What's the problem?



; , , , . , :



for (int iter=0; iter<nb_iter; iter++) {          // some iterative computation
    for (int c=0; c<mesh.cells.nb(); c++)         // loop through all tetrahedra
        for (int lv0=0; lv0<4; lv0++)             // for every pair of
            for (int lv1 = lv0+1; lv1<4; lv1++)   // vertices in the tet
                for (int d=0; d<3; d++) {         // do stuff for each of 3 dimensions
                    nlRowScaling(weight);
                    nlBegin(NL_ROW);
                    nlCoefficient(mesh.cells.vertex(c, lv0)*3 + d,  1);
                    nlCoefficient(mesh.cells.vertex(c, lv1)*3 + d, -1);
                    nlEnd(NL_ROW);
                }
    [...]
}


, , . , , --, .



; . ( ) , for (int i=0; i<size; i++) — : for , .



for(;;), : , . , 0 size-1, . , ? , — .



c++11 , :



#define FOR(I,UPPERBND) for(int I = 0; I<int(UPPERBND); ++I)


:



FOR(iter, nb_iter) {
    FOR(c, mesh.cells.nb())
        FOR(lv0, 4)
            for (int lv1 = lv0+1; lv1<4; lv1++)
                FOR(d, 3) {
                    nlRowScaling(weight);
                    nlBegin(NL_ROW);
                    nlCoefficient(mesh.cells.vertex(c, lv0)*3 + d,  1);
                    nlCoefficient(mesh.cells.vertex(c, lv1)*3 + d, -1);
                    nlEnd(NL_ROW);
                }
    [...]
}


, for (;;), , (, , ). FOR(,) 0 n-1 - . , , , (. ), (, , ).



, , , : " , ?"



11 , range for



? , . , :



for i in range(n):
    print(i)


, range() , . c++11 !



#include <iostream>
int main() {
    int range[] = {0,1,2,3,4,5};
    for (int i : range) {
        std::cerr << i;
    }
}


, , . C++ !



range(int n):



#include <iostream>

constexpr auto range(int n) {
    struct iterator {
        int i;
        void operator++() { ++i; }
        bool operator!=(const iterator& rhs) const { return i != rhs.i; }
        const int &operator*() const { return i; }
    };
    struct wrapper {
        int n;
        auto begin() { return iterator{0}; }
        auto end()   { return iterator{n}; }
    };
    return wrapper{n};
}

int main() {
    for (int i: range(13)) {
        std::cerr << i;
    }
    return 0;
}


, int vs size_t, . gcc 10.2 -std=c++17 -Wall -Wextra -pedantic -O1, ( ):



[...]
.L2:
        mov     esi, ebx
        mov     edi, OFFSET FLAT:_ZSt4cerr
        call    std::basic_ostream<char, std::char_traits<char> >::operator<<(int)
        add     ebx, 1
        cmp     ebx, 13
        jne     .L2
[...]


, , for (int i=0; i<13; i++).



, for (int i: range(n)) , FOR(,), , .



: enumerate



Range for c++11 . , , , :



#include <vector>
#include <iostream>

struct vec3 {  double x,y,z;  };

int main() {
    std::vector<vec3> points = {{6,5,8},{1,2,3},{7,3,7}};
    for (vec3 &p: points) {
        std::cerr << p.x;
    }
    return 0;
}


for (vec3 &p: points) , , . , ? , . , , , :



    std::vector<vec3> points = {{6,5,8},{1,2,3},{7,3,7}};
    std::vector<double> weights = {4,6,9};
    int i = 0;
    for (vec3 &p: points) {
        std::cerr << p.x << weights[i++];
    }


:



asm
.L2:
        movsd   xmm0, QWORD PTR [r13+0]
        mov     edi, OFFSET FLAT:_ZSt4cerr
        call    std::basic_ostream<char, std::char_traits<char> >& std::basic_ostream<char, std::char_traits<char> >::_M_insert<double>(double)
        movsd   xmm0, QWORD PTR [rbp+0]
        mov     rdi, rax
        call    std::basic_ostream<char, std::char_traits<char> >& std::basic_ostream<char, std::char_traits<char> >::_M_insert<double>(double)
        add     rbp, 8
        add     r13, 24
        cmp     r14, rbp
        jne     .L2


, , , , , c++17 structural binding!



, :



#include <vector>
#include <iostream>
#include "range.h"

struct vec3 {
    double x,y,z;
};

int main() {
    std::vector<vec3> points = {{6,5,8},{1,2,3},{7,3,7}};
    std::vector<double> weights = {4,6,9};

    for (auto [i, p]: enumerate(points)) {
        std::cerr << p.x << weights[i];
    }

    return 0;
}


enumerate() :



range.h
#ifndef __RANGE_H__
#define __RANGE_H__

#include <tuple>
#include <utility>
#include <iterator>

constexpr auto range(int n) {
    struct iterator {
        int i;
        void operator++() { ++i; }
        bool operator!=(const iterator& rhs) const { return i != rhs.i; }
        const int &operator*() const { return i; }
    };
    struct wrapper {
        int n;
        auto begin() { return iterator{0}; }
        auto end()   { return iterator{n}; }
    };
    return wrapper{n};
}

template <typename T> constexpr auto enumerate(T && iterable) {
    struct iterator {
        int i;
        typedef decltype(std::begin(std::declval<T>())) iterator_type;
        iterator_type iter;
        bool operator!=(const iterator& rhs) const { return iter != rhs.iter; }
        void operator++() { ++i; ++iter; }
        auto operator*() const { return std::tie(i, *iter); }
    };
    struct wrapper {
        T iterable;
        auto begin() { return iterator{0, std::begin(iterable)}; }
        auto end()   { return iterator{0, std::end  (iterable)}; }
    };
    return wrapper{std::forward<T>(iterable)};
}

#endif // __RANGE_H__


-std=c++17 -Wall -Wextra -pedantic -O2 ( ):



ASM
.L14:
        movsd   xmm0, QWORD PTR [rbx]
        mov     edi, OFFSET FLAT:_ZSt4cerr
        call    std::basic_ostream<char, std::char_traits<char> >& std::basic_ostream<char, std::char_traits<char> >::_M_insert<double>(double)
        mov     rdi, rax
        mov     rax, QWORD PTR [rsp+32]
        movsd   xmm0, QWORD PTR [rax+rbp]
        call    std::basic_ostream<char, std::char_traits<char> >& std::basic_ostream<char, std::char_traits<char> >::_M_insert<double>(double)
        add     rbx, 24
        add     rbp, 8
        cmp     r12, rbx
        jne     .L14


And again, the compiler cleanly removed the wrapper (however, for this it was necessary to raise the optimization level from -O1to -O2).

By the way, it appeared in c ++ 20 std::ranges, which makes it even easier to write such a function, but I'm not yet ready to switch to this standard.



Question to the audience



In your opinion, what should be the perfect cycle in 2020? Teach me!



If you have not asked this question yet, then copy the header file to your pet-project range.hand try to use it for at least a few days.




All Articles