fold expressions und vector arithmetik



  • http://en.cppreference.com/w/cpp/language/fold

    vector& operator*=(const double& value)
    {
    m_data[0] *= value;
    m_data[1] *= value;
    m_data[2] *= value;
    return *this;
    }

    vector& operator/=(const double& value)
    {
    m_data[0] /= value;
    m_data[1] /= value;
    m_data[2] /= value;
    return *this;
    }

    double m_data[3];

    ist es möglich mit C++11/14/17 fold expressions einfache Vektor Rechnung wie die oben Zwangs zu Loop-Unrolen - mir ist klar das die meisten Kompiler das auch machen wenn ich eine for-Schleife nutze - oder macht das keinen Sinn wenn nur die Menge fix ist aber die Inhalte variant?


  • Mod

    Natürlich. man muss nur ein entsprechendes Templateparameterpack herzaubern, um es dann entpacken zu können.
    quick&dirty z.B.:

    template <typename = std::make_index_sequence<3>> class my_vector;
    template <std::size_t... I> class my_vector<std::index_sequence<I...>> {
        ...
        my_vector& operator*=(double value)
        {
            return ( m_data[I] *= value, ..., *this );
        }
    
        my_vector& operator/=(double value)
        {
            return ( m_data[I] /= value, ..., *this );
        }
    
        double m_data[sizeof...(I)];
    };
    

    fold-Ausdrücke gibt es übrigens erst ab C++17



  • fold-Ausdrücke gibt es übrigens erst ab C++17

    😞

    trotzdem Danke



  • die Zeile mit

    return ( m_data[I] Operator value, ..., *this );
    

    ?

    wiederholt sich ja fuer einige Funktionen

    könnte man das auch irgendwie von aussen einbringen?

    z.B. mit einem helper oder sowas

    auf_alle_anwenden(Operator)

    return ( m_data[I] Operator value, ..., *this );
    
    my_vector& operator/=(double value)
        {
            return auf_alle_anwenden(/=);
        }
    

    oder gibt es da einen Trick aka bessere Lösung


  • Mod

    Gast3 schrieb:

    die Zeile mit

    return ( m_data[I] Operator value, ..., *this );
    

    ?

    wiederholt sich ja fuer einige Funktionen

    könnte man das auch irgendwie von aussen einbringen?

    z.B. mit einem helper oder sowas

    auf_alle_anwenden(Operator)

    return ( m_data[I] Operator value, ..., *this );
    
    my_vector& operator/=(double value)
        {
            return auf_alle_anwenden(/=);
        }
    

    Nutzen?



  • Nutzen?

    Möchte nur gerne verstehe wie atomar man Code-Reuse bei Fold-Expressions sinvoll verwenden kann

    da man einen Operator nicht als Template-Type übergeben kann müsste man dann wohl einen "/=" durchführenden Funktion oder sowas machen - oder?



  • Hallo,

    sowas könnte man machen; heißt nicht, dass man sollte:

    #include <cstddef>
    #include <utility>
    
    template< typename... args_t >
    void discard( args_t&&... args ) {
    }
    
    template< std::size_t N >
    struct vec {
    	double data[ N ];
    
    	template< typename functor_t, std::size_t... indices >
    	void apply( functor_t&& functor, vec const& rhs, std::integer_sequence< std::size_t, indices... > ) {
    		discard( ( functor( data[ indices ], rhs.data[ indices ] ), 0 )... );
    	}
    	template< typename functor_t >
    	void apply( functor_t&& functor, vec const& rhs ) {
    		apply( std::forward< functor_t >( functor ), rhs, std::make_index_sequence< N >{} );
    	}
    
    	template< typename functor_t, std::size_t... indices >
    	void apply( functor_t&& functor, std::integer_sequence< std::size_t, indices... > ) {
    		discard( ( functor( data[ indices ] ), 0 )... );
    	}
    	template< typename functor_t >
    	void apply( functor_t&& functor ) {
    		apply( std::forward< functor_t >( functor ), std::make_index_sequence< N >{} );
    	}
    
    	void operator+=( vec const& rhs ) {
    		apply( []( auto& x, auto&& y ){ x += y; }, rhs );
    	}
    	void operator*=( double y ) {
    		apply( [ y ]( auto& x ){ x *= y; } );
    	}
    };
    
    void f( vec< 4 >& x, vec< 4 > const& y ) {
    	x += y;
    }
    void g( vec< 4 >& x, double y ) {
    	x *= y;
    }
    

    Das ist ohne Fold Expressions aber mit dem Index-Sequence-Trick aus C++14.
    Die Multiplikation wird korrekt vektorisiert, die Addition leider nicht: https://godbolt.org/g/H2gcWn

    LG


  • Mod

    Fytch schrieb:

    Das ist ohne Fold Expressions aber mit dem Index-Sequence-Trick aus C++14.
    Die Multiplikation wird korrekt vektorisiert, die Addition leider nicht: https://godbolt.org/g/H2gcWn

    Kann man alles haben, und einfacher.

    #include <cstddef>
    #include <utility>
    
    template <typename... args_t>
    void discard(args_t&&... args) {
    }
    
    template <std::size_t N, typename = std::make_index_sequence<N>>
    struct vec;
    
    template <std::size_t N, std::size_t... I>
    struct vec<N, std::index_sequence<I...>> {
    	double data[ N ];
    
    	vec& operator+=(vec const& rhs) {
    		return *this = vec{ { data[I] + rhs.data[I] ... } };
    	}
    	vec& operator*=(double y) {
    		discard( data[I] *= y ... );
    		return *this;
    	}
    };
    
    void f( vec< 4 >& x, vec< 4 > const& y ) {
        x += y;
    }
    void g( vec< 4 >& x, double y ) {
        x *= y;
    }
    

    https://godbolt.org/g/cpHxWl



  • Ich bezog mich auf die Frage:

    Gast3 schrieb:

    könnte man das auch irgendwie von aussen einbringen?

    z.B. mit einem helper oder sowas

    auf_alle_anwenden(Operator)

    Aber die klassenweite Index-Liste ist definitiv schick.


  • Mod

    Fytch schrieb:

    Ich bezog mich auf die Frage:

    Gast3 schrieb:

    könnte man das auch irgendwie von aussen einbringen?

    z.B. mit einem helper oder sowas

    auf_alle_anwenden(Operator)

    Aber die klassenweite Index-Liste ist definitiv schick.

    Auch deinen Code kann man vektorisiert bekommen, wenn man dem Compiler in Hinblick auf Aliasing hilft mit ein bisschen Dimensionsanalyse

    #include <tuple>
    #include <utility>
    
    template <typename T, std::size_t D>
    struct quantity {
    	T value;
    	constexpr quantity(T v) : value(v) {}
    };
    template <typename T, std::size_t D>
    quantity<T, D>& operator+=(quantity<T,D>& lhs, const quantity<T,D>& rhs) {
    	lhs.value += rhs.value;
    	return lhs;
    }
    template <typename T, std::size_t D>
    quantity<T, D>& operator*=(quantity<T,D>& lhs, T rhs) {
    	lhs.value += rhs;
    	return lhs;
    }
    
    template <typename T, typename U> struct quantities_impl;
    template <typename T, std::size_t... I>
    struct quantities_impl<T, std::index_sequence<I...>> {
    	using type = std::tuple<quantity<T, I>...>;
    };
    template <typename T, std::size_t N>
    using quantities = typename quantities_impl<T, std::make_index_sequence<N>>::type;
    
    template< typename... args_t >
    void discard( args_t&&... args ) {
    }
    
    template< std::size_t N >
    struct vec {
        quantities <double, N> data;
    
    	template <typename... T>
    	vec(T... v)
    		: data(v...)
    	{}
        template< typename functor_t, std::size_t... indices >
        void apply( functor_t&& functor, vec const& rhs, std::integer_sequence< std::size_t, indices... > ) {
            discard( ( functor( std::get<indices>(data), std::get<indices>(rhs.data) ), 0 )... );
        }
        template< typename functor_t >
        void apply( functor_t&& functor, vec const& rhs ) {
            apply( std::forward< functor_t >( functor ), rhs, std::make_index_sequence< N >{} );
        }
    
        template< typename functor_t, std::size_t... indices >
        void apply( functor_t&& functor, std::integer_sequence< std::size_t, indices... > ) {
            discard( ( functor( std::get<indices>(data) ), 0 )... );
        }
        template< typename functor_t >
        void apply( functor_t&& functor ) {
            apply( std::forward< functor_t >( functor ), std::make_index_sequence< N >{} );
        }
    
        void operator+=( vec const& rhs ) {
            apply( []( auto& x, auto&& y ){ x += y; }, rhs );
        }
        void operator*=( double y ) {
            apply( [ y ]( auto& x ){ x *= y; } );
        }
    };
    
    void f( vec< 4 >& x, vec< 4 > const& y ) {
        x += y;
    }
    void g( vec< 4 >& x, double y ) {
        x *= y;
    }
    

    https://godbolt.org/g/YuBU55



  • vielen Dank - sehr gute Tips - jetzt muss ich nur noch den Kompiler upgraden
    - Erfahrung wie der Microsoft-Kompiler so mitspielt - bisher sehen die Ergebnisse verglichen mit gcc/clang nicht so berauschend aus



  • Als weitere Inspiration, hier noch ein alternativer Ansatz, den ich mal in einer Matrixbibliothek implementiert habe, die ich für die Grafikprogrammierung verwende:

    #include <cstddef>
    #include <iostream>
    #include <type_traits>
    
    template <std::size_t N>
    auto operator<<(std::ostream& out, const double (&elements)[N]) -> std::ostream&
    {
        out << "{ " << elements[0];
        for (std::size_t i = 1; i < N; ++i)
            out << ", " << elements[i];
        out << " }";
        return out;
    }
    
    template <typename LHS, typename RHS>
    struct MulOp
    {
        inline void operator()(LHS& e, const RHS& value) const
        {
            e *= value;
        }
    };
    
    template <typename LHS, typename RHS>
    struct DivOp
    {
        inline void operator()(LHS& e, const RHS& value) const
        {
            e /= value;
        }
    };
    
    template <template <typename, typename> class OP>
    struct auf_alle_anwenden
    {
        template <typename LHS, typename RHS>
        inline void operator()(LHS& elements, const RHS& value) const
        {
            for (std::size_t i = 0; i < std::extent<LHS>::value; ++i)
                OP<std::remove_extent_t<LHS>, RHS>{}(elements[i], value);    
        }
    };
    
    template <template <typename, typename> class OP, typename LHS, typename RHS, std::size_t I, typename = void>
    struct UnrollOp
    {
        inline void operator()(LHS&, const RHS& value) const
        {
        }
    };
    
    template <template <typename, typename> class OP, typename LHS, typename RHS, std::size_t I>
    struct UnrollOp<OP, LHS, RHS, I, std::enable_if_t<(I < std::extent<LHS>::value)>>
    {
        inline void operator()(LHS& elements, const RHS& value) const
        {
            OP<std::remove_extent_t<LHS>, RHS>{}(elements[I], value);
            UnrollOp<OP, LHS, RHS, I + 1>{}(elements, value);
        }
    };
    
    template <template <typename, typename> class OP>
    struct auf_alle_anwenden_unroll
    {
        template <typename LHS, typename RHS>
        inline void operator()(LHS& elements, const RHS& value) const
        {
            UnrollOp<OP, LHS, RHS, 0>{}(elements, value);
        }
    };
    
    auto main() -> int
    {
        double a[] = { 1, 2, 3, 4 };
        std::cout << a << std::endl;
        auf_alle_anwenden<MulOp>{}(a, 2.5);
        std::cout << a << std::endl;
        auf_alle_anwenden<DivOp>{}(a, 2.5);
        std::cout << a << std::endl;
        auf_alle_anwenden_unroll<MulOp>{}(a, 2.5);
        std::cout << a << std::endl;
        auf_alle_anwenden_unroll<DivOp>{}(a, 2.5);
        std::cout << a << std::endl;
        return 0;
    }
    

    Der Vorteil hierbei ist, dass die "Operations"-Templates recht flexibel spezialisiert werden können. In meiner eigentlichen Implementation ist z.B. auch noch
    eine Spezialisierung über die Operations-Breite möglich, so dass ich über #ifdef -Konstrukte auch explizite Vektorisierungen mit SSE/AVX-Compiler Intrinsics
    aktivieren kann. Der Code sucht sich dann via TMP die breiteste Spezialisierung einer Operation heraus, und wendet sie dann auf das Array an.
    Natürlich ist das Beispiel hier nur die lowlevel-Implementation, für den Anwender ist alles transparent in Matrix/Vektor-Klassen verpackt.

    Bezüglich explizitem Unrolling: Das wird hier erreicht, indem die Schleife in eine TMP-Endrekursion überführt wurde. Das funktioniert meiner Erfahrung nach
    in der Praxis ziemlich zuverlässig, da der Compiler ansonsten selbständig eine Schleife generieren müsste wo im Code keine ist (ist mir bisher zumindest
    noch nicht untergekommen, dass Compiler eine deratige Optimierung gemacht hätten).

    Gruss,
    Finnegan



  • Vielen Dank - wirklich sehr informativ


Log in to reply