Boosting Performance in Flutter with FFI: A Step-by-Step Guide (iOS example) ???

Boosting Performance in Flutter with FFI: A Step-by-Step Guide (iOS example) ???

Foreign Function Interface (FFI) in Flutter allows Dart to communicate with native code like C/C++. This can dramatically improve the performance of computation-heavy tasks, making your Flutter applications more efficient. In this guide, we will walk you through how to integrate FFI into your Flutter app by using an example—matrix multiplication in both Dart and C—and compare their performance.


Why FFI Matters

  • Enhanced Performance: Native languages like C are significantly faster for CPU-bound tasks such as matrix multiplication, cryptography, image editing, and more.
  • Efficiency: Leveraging C libraries for existing performance-critical operations offers substantial efficiency improvements.
  • Scalability: Offloading heavy computations to native code allows your Flutter app to handle more complex tasks without compromising on speed.


Step 1: Understanding FFI

What is FFI?

FFI (Foreign Function Interface) is a mechanism by which Dart code can call functions written in other programming languages, such as C, C++, or Rust. This is especially helpful when dealing with performance-critical tasks that Dart might struggle with due to its higher-level abstractions.


Step 2: Project Setup

What We're Doing:

Setting up a new Flutter project to serve as the foundation for our performance test.

How to Do It:

Create a New Flutter Project:

Open your terminal and run:

flutter create  -e ffi_performance_test
cd ffi_performance_test        

Add the FFI Package:

Open your pubspec.yaml file and add the ffi package under dependencies:

dependencies:
  flutter:
    sdk: flutter
  ffi: ^2.1.3 #add this line        

Install Dependencies:

Run the following command to fetch the new dependencies:

flutter pub get        

Step 3: Writing the Native C Code

What We're Doing:

Creating a simple C function for matrix multiplication. This function takes two matrices, multiplies them, and stores the result in a third matrix.

How to Do It:

  • Open Xcode Project:

Navigate to the ios directory of your Flutter project and open Runner.xcworkspace in Xcode.

  1. Add C File:

Right-click on the Runner folder in the Project Navigator.

Select Add Files to "Runner"...

Choose C File from the file template options.

Name the file matrix_multiplication.c and click Create.

Paste the C Code:

  • Edit matrix_multiplication.h:

Open the newly created matrix_multiplication.h file and add the following code:

#ifndef MATRIX_MULTIPLICATION_H
#define MATRIX_MULTIPLICATION_H

#ifdef __cplusplus
extern "C" {
#endif

// Export the function for FFI and prevent it from being stripped
__attribute__((visibility("default")))
__attribute__((used))
void multiply_matrices(int* A, int* B, int* result, int N);

#ifdef __cplusplus
}
#endif

#endif // MATRIX_MULTIPLICATION_H        

  • Edit matrix_multiplication.c:

Open the newly created matrix_multiplication.c file and add the following code:

#include "matrix_multiplication.h"

// Prevent name mangling if compiled with a C++ compiler
#ifdef __cplusplus
extern "C" {
#endif

// Ensure the function is exported and not stripped
__attribute__((visibility("default")))
__attribute__((used))
void multiply_matrices(int* A, int* B, int* result, int N) {
    for (int i = 0; i < N; i++) {
        for (int j = 0; j < N; j++) {
            result[i * N + j] = 0;
            for (int k = 0; k < N; k++) {
                result[i * N + j] += A[i * N + k] * B[k * N + j];
            }
        }
    }
}

#ifdef __cplusplus
}
#endif        

Explanation:

Header File (matrix_multiplication.h):

  • Purpose: The header file declares the multiply_matrices function, making it visible to other files, including Dart via FFI.
  • Include Guards (#ifndef, #define, #endif): Prevent multiple inclusions of the header file, which can cause compilation errors.

extern "C" Block: Ensures that if the code is compiled with a C++ compiler, the function names are not mangled. Name mangling can prevent Dart from correctly locating the C functions.

Visibility Attributes:

  1. __attribute__((visibility("default"))): Makes the multiply_matrices function visible outside the shared library, which is essential for FFI to locate and invoke it.
  2. __attribute__((used)): Prevents the compiler from optimizing away the function, ensuring it remains available for FFI calls.

Source File (matrix_multiplication.c):

  • Including the Header:

#include "matrix_multiplication.h"        

Ensures that the function declaration matches the implementation, maintaining consistency and preventing linkage issues.

  • Function Implementation:

void multiply_matrices(int* A, int* B, int* result, int N) {
    for (int i = 0; i < N; i++) {
        for (int j = 0; j < N; j++) {
            result[i * N + j] = 0;
            for (int k = 0; k < N; k++) {
                result[i * N + j] += A[i * N + k] * B[k * N + j];
            }
        }
    }
}        

Parameters:

  • int* A: Pointer to the first input matrix.
  • int* B: Pointer to the second input matrix.
  • int* result: Pointer to the matrix where the result will be stored.
  • int N: The size of the matrices (assuming square matrices).

Functionality:

  • Iterates through each element of the resulting matrix.
  • Performs the multiplication and accumulation of corresponding elements from matrices A and B.

Build the C Code:

Note: Xcode automatically handles the linking of the C library when you run the project, so there's no need for manual linking steps.


Step 4: Dart Integration with FFI

What We're Doing:

Writing Dart code to integrate the native C function using the FFI package.

How to Do It:

Create the Dart FFI Wrapper:

  • Create a New Dart File:

Inside the lib directory, create a new file named matrix_multiplication_ffi.dart.

  • Add the Following Code:

import 'dart:ffi' as ffi;
import 'package:ffi/ffi.dart'; // Required for malloc and free

typedef MatrixMultiplyNative = ffi.Void Function(
    ffi.Pointer<ffi.Int32>, ffi.Pointer<ffi.Int32>, ffi.Pointer<ffi.Int32>, ffi.Int32);
typedef MatrixMultiplyDart = void Function(
    ffi.Pointer<ffi.Int32>, ffi.Pointer<ffi.Int32>, ffi.Pointer<ffi.Int32>, int);

class MatrixMultiplicationFFI {
  late ffi.DynamicLibrary _lib;
  late MatrixMultiplyDart _multiply;

  MatrixMultiplicationFFI() {
    // Load the C library
    _lib = ffi.DynamicLibrary.process();

    // Lookup for the matrix multiplication function
    _multiply = _lib
        .lookup<ffi.NativeFunction<MatrixMultiplyNative>>('multiply_matrices')
        .asFunction();
  }

  void multiply(List<int> A, List<int> B, List<int> result, int N) {
    // malloc is used to request memory from the computer's heap (a region of      memory reserved for dynamic allocation).
    final pointerA = malloc.allocate<ffi.Int32>(A.length * ffi.sizeOf<ffi.Int32>());
    final pointerB = malloc.allocate<ffi.Int32>(B.length * ffi.sizeOf<ffi.Int32>());
    final pointerResult = malloc.allocate<ffi.Int32>(result.length * ffi.sizeOf<ffi.Int32>());

    // Copy Dart data into allocated memory
    for (int i = 0; i < A.length; i++) {
      pointerA[i] = A[i];
    }
    for (int i = 0; i < B.length; i++) {
      pointerB[i] = B[i];
    }

    // Call the C function via FFI
    _multiply(pointerA, pointerB, pointerResult, N);

    // Copy result from native memory back to Dart list
    for (int i = 0; i < result.length; i++) {
      result[i] = pointerResult[i];
    }

    // Free allocated memory
    malloc.free(pointerA);
    malloc.free(pointerB);
    malloc.free(pointerResult);
  }
}        

  • Explanation:

Typedefs:

MatrixMultiplyNative: Defines the C function signature.

MatrixMultiplyDart: Defines the Dart function signature corresponding to the C function.

MatrixMultiplicationFFI Class:

_lib: Loads the dynamic library containing the C functions.

_multiply: Maps the C multiply_matrices function to a Dart function.

multiply Method:

Allocates memory for input matrices A and B, and the result matrix.

Copies Dart list data into the allocated native memory.

Calls the C function via FFI.

Copies the result back from native memory to the Dart list.

Frees the allocated memory to prevent memory leaks.


Step 5: Writing the Flutter App for Performance Comparison

What We're Doing:

Creating a simple Flutter app that multiplies matrices using both Dart and C (via FFI) and compares the performance of both implementations.

How to Do It:

Update main.dart:

Replace the content of lib/main.dart with the following code:

import 'dart:math';
import 'package:flutter/material.dart';
import 'matrix_multiplication_ffi.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: const Text('Matrix Multiplication Performance')),
        body: const PerformanceComparison(),
      ),
    );
  }
}

class PerformanceComparison extends StatefulWidget {
  const PerformanceComparison({super.key});

  @override
  State<PerformanceComparison> createState() => _PerformanceComparisonState();
}

class _PerformanceComparisonState extends State<PerformanceComparison> {
  final int _matrixSize = 1000;
  late List<int> _matrixA;
  late List<int> _matrixB;
  late List<int> _result;
  late MatrixMultiplicationFFI _ffi;
  int _dartTime = 0;
  int _cTime = 0;

  @override
  void initState() {
    super.initState();
    _matrixA = List.generate(_matrixSize * _matrixSize, (_) => Random().nextInt(100));
    _matrixB = List.generate(_matrixSize * _matrixSize, (_) => Random().nextInt(100));
    _result = List.filled(_matrixSize * _matrixSize, 0);
    _ffi = MatrixMultiplicationFFI();
  }

  void _multiplyInDart() async {
    setState(() {
      _dartTime = 0;
    });

    Stopwatch stopwatch = Stopwatch()..start();

    // Matrix multiplication in Dart
    for (int i = 0; i < _matrixSize; i++) {
      for (int j = 0; j < _matrixSize; j++) {
        _result[i * _matrixSize + j] = 0;
        for (int k = 0; k < _matrixSize; k++) {
          _result[i * _matrixSize + j] +=
              _matrixA[i * _matrixSize + k] * _matrixB[k * _matrixSize + j];
        }
      }
    }

    stopwatch.stop();

    setState(() {
      _dartTime = stopwatch.elapsedMilliseconds;
    });
  }

  void _multiplyInC() async {
    setState(() {
      _cTime = 0;
    });

    Stopwatch stopwatch = Stopwatch()..start();

    _ffi.multiply(_matrixA, _matrixB, _result, _matrixSize);

    stopwatch.stop();
    setState(() {
      _cTime = stopwatch.elapsedMilliseconds;
    });
  }

  @override
  Widget build(BuildContext context) {
    return SafeArea(
      child: Container(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          children: [
            Center(
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.center,
                children: [
                  Text('Matrix size: $_matrixSize x $_matrixSize',
                      style: const TextStyle(
                          fontSize: 20, fontWeight: FontWeight.bold)),
                  Text(
                      'Loop Control Overheads: ${_matrixSize * _matrixSize * _matrixSize}',
                      style: const TextStyle(fontSize: 16)),
                ],
              ),
            ),
            const SizedBox(height: 30),
            const Expanded(
              child: Center(
                  child: Text(
                'Let\'s multiply some matrices!',
                style: TextStyle(fontSize: 20),
              )),
            ),
            const SizedBox(height: 30),
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                const Text('Dart multiplication took: ',
                    style: TextStyle(fontSize: 16)),
                Text('${_dartTime}ms',
                    style: const TextStyle(
                        fontSize: 20, fontWeight: FontWeight.bold)),
              ],
            ),
            const SizedBox(height: 10),
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                const Text('C multiplication (FFI) took: ',
                    style: TextStyle(fontSize: 16)),
                Text('${_cTime}ms',
                    style: const TextStyle(
                        fontSize: 20, fontWeight: FontWeight.bold)),
              ],
            ),
            const SizedBox(height: 30),
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
              children: [
                ElevatedButton(
                  onPressed: _multiplyInDart,
                  child: const Text('Multiply in Dart'),
                ),
                ElevatedButton(
                  onPressed: _multiplyInC,
                  child: const Text('Multiply in C (FFI)'),
                ),
              ],
            ),
            const SizedBox(height: 30),
          ],
        ),
      ),
    );
  }
}        

Explanation:

  • Matrix Initialization:

_matrixA and _matrixB are initialized with random integers.

_result is a list to store the multiplication result.

  • MatrixMultiplicationFFI Instance:

_ffi is an instance of the FFI wrapper class to interact with the native C function.

  • Multiplication Methods:

_multiplyInDart: Performs matrix multiplication purely in Dart and measures the time taken.

_multiplyInC: Calls the native C function via FFI to perform matrix multiplication and measures the time taken.

  • UI Components:

Displays the matrix size and the number of loop iterations (for context).

Shows the time taken for both Dart and C implementations.

Provides buttons to trigger each multiplication method.

  • Performance Measurement:

Uses Stopwatch to measure the elapsed time for each multiplication method.

Updates the UI with the results using setState.


Try it:

flutter run lib/main.dart --release        

Conclusion

Congratulations! You've successfully offloaded performance-critical tasks like matrix multiplication to native C code using FFI in Flutter. By integrating native code, you've demonstrated a significant performance improvement over Dart's implementation.



As illustrated in the screenshot above, the C implementation outperforms the Dart version, showcasing the efficiency gains achieved through FFI.

Note: In this example, the app's interface will freeze until the calculation completes. To enhance user experience, consider performing these operations asynchronously or offloading them to separate isolates to prevent UI blocking.


Next Steps

  • Experiment: Try extending the app with implementing the Android side.
  • Explore FFI Further: Dive deeper into FFI's capabilities to enhance your apps.


Did you find this tutorial helpful? Share your thoughts or ask questions below!

Happy coding!


要查看或添加评论,请登录

Vitalii Vyrodov的更多文章

社区洞察

其他会员也浏览了