Boggle solver in Rust - Looking for speedup
The main thing that's going to get you a speed boost is returning early from the search if there's no word that starts with what you have so far.

One simple way to do this is to keep a HashSet of all the prefixes of words in the dictionary. Or, better yet, a HashMap where the value indicates whether the prefix is actually a word or not.

struct PrefixMap&lt;'a&gt;(HashMap&lt;&amp;'a str, bool&gt;);

impl&lt;'a&gt; PrefixMap&lt;'a&gt; {
    fn new(words: impl IntoIterator&lt;Item = &amp;'a str&gt;) -&gt; Self {
        let mut map = HashMap::new();
        for word in words {
            // note that this gets every prefix except the whole word
            for (ending, _) in word.char_indices() {
                map.entry(&amp;word[..ending]).or_insert(false);
            }
            map.insert(word, true);
        }
        Self(map)
    }

    fn is_word(&amp;self, word: &amp;str) -&gt; bool {
        self.0.get(word).copied().unwrap_or(false)
    }

    fn is_prefix(&amp;self, prefix: &amp;str) -&gt; bool {
        self.0.contains_key(prefix)
    }
}


Then, in the first part of boggle_solve_help, we can use is_word and is_prefix to determine whether to print the word or return early.

if !dict.is_prefix(&amp;word) {
    return;
}

if word.len() &gt; 3 &amp;&amp; dict.is_word(&amp;word) {
    println!("{}", word);
}


While this isn't the most efficient thing possible, that simple step makes the program run almost instantly on my computer (less than a second). If you still want to improve more, look up tries for a memory efficient and quick way to do the same thing. Since creating the index of words takes a little while, you might try to cache it between runs using serde or something similar.



Now for some more suggestions.


Derive PartialEq. Right now you implement PartialEq for Coordinate manually, but the same implementation can be done by adding PartialEq to the derive attribute.
Use descriptive variable names. The main offenders are the arguments to move_c and later on c in boggle_solve_help. I know sometimes this is annoying, but even just changing a and b to dy and dx (calculus shorthand for "change in y/x") makes everything clearer. It's probably worth spelling out height and width to make it clear that that's what you mean.
Don't forget to use cargo fmt. It's a simple step and will instantly make your code more standardized and easier to read for other Rust programmers.
Encapsulate the grid into its own type. Right now, you're using Vec&lt;Vec&lt;char&gt;&gt; directly, which is fine, but means that lots of annoying details are strewn about the functions that use it. The type can still be Vec&lt;Vec&lt;char&gt;&gt; under the surface, but having a method like grid.get(coordinate) rather than grid[coordinate.row as usize][coordinate.col as usize] makes things easier to read and make sense of. As a plus, this will also silence Clippy's complaint about using &amp;Vec&lt;_&gt; instead of &amp;[_].
Consider switching to an iterative algorithm. You won't run into stack overflow here since you have a hard cap on how deep the recursion can go. Still, if you want a slightly faster algorithm, a depth-first search can be memory efficient and avoid the repeated String allocations you currently have (with format!).
Bonus: don't show duplicate words. Right now, a word will be duplicated if can be spelled with a different path of letters on the board. For example, "trees" can be written three different ways near the bottom right corner of the board so it gets printed 3 times. It also shows up in a few other places. I might suggest keeping a HashSet of all the words you've found so far to eliminate duplicates.


Here's the code after applying the early return and the first three of my suggestions. I'll let you figure out the other two.

use std::collections::HashMap;
use std::fs;

const MAX_WORD_LEN: usize = 12;

#[derive(Clone, Debug, Copy, PartialEq)]
struct Coordinate {
    row: i64,
    col: i64,
}

impl Coordinate {
    // Returns a new coordinate if the move is valid. None otherwise.
    fn move_coord(&amp;self, dy: i64, dx: i64, height: i64, width: i64) -&gt; Option&lt;Coordinate&gt; {
        let height_range = 0..height;
        let width_range = 0..width;
        if height_range.contains(&amp;(self.row + dy)) &amp;&amp; width_range.contains(&amp;(self.col + dx)) {
            Some(Coordinate {
                row: self.row + dy,
                col: self.col + dx,
            })
        } else {
            None
        }
    }
}

// Keys consist of all prefixes of words
// Values say whether the prefix is a word
// Since we keep references rather than `String`s,
// the lifetime of the map can't exceed the lifetime of wherever
// the `&amp;str`s come from.
// If you decide to make this more persistent, using `String` rather than `&amp;str`
// might be appropriate.
struct PrefixMap&lt;'a&gt;(HashMap&lt;&amp;'a str, bool&gt;);

impl&lt;'a&gt; PrefixMap&lt;'a&gt; {
    // We can produce this from any iterator of `&amp;str`s.
    fn new(words: impl IntoIterator&lt;Item = &amp;'a str&gt;) -&gt; Self {
        let mut map = HashMap::new();
        for word in words {
            // note that this gets every prefix except the whole word
            for (ending, _) in word.char_indices() {
                map.entry(&amp;word[..ending]).or_insert(false);
            }
            map.insert(word, true);
        }
        Self(map)
    }

    fn is_word(&amp;self, word: &amp;str) -&gt; bool {
        self.0.get(word).copied().unwrap_or(false)
    }

    fn is_prefix(&amp;self, prefix: &amp;str) -&gt; bool {
        self.0.contains_key(prefix)
    }
}

fn boggle_solve(grid: &amp;Vec&lt;Vec&lt;char&gt;&gt;, dict: &amp;PrefixMap) {
    for (i, row) in grid.iter().enumerate() {
        for (j, ch) in row.iter().enumerate() {
            boggle_solve_help(
                grid,
                dict,
                ch.to_string(),
                Coordinate {
                    row: i as i64,
                    col: j as i64,
                },
                vec![Coordinate {
                    row: i as i64,
                    col: j as i64,
                }],
            )
        }
    }
}

fn boggle_solve_help(
    grid: &amp;Vec&lt;Vec&lt;char&gt;&gt;,
    dict: &amp;PrefixMap,
    word: String,
    coord: Coordinate,
    path: Vec&lt;Coordinate&gt;,
) {
    // Base case.
    // No more work if the string is long enough
    if word.len() &gt; MAX_WORD_LEN {
        return;
    }

    if !dict.is_prefix(&amp;word) {
        return;
    }

    if word.len() &gt; 3 &amp;&amp; dict.is_word(&amp;word) {
        println!("{}", word);
    }

    // For every possible direction the path can take, if it is valid, take it.
    for dy in -1..=1 {
        for dx in -1..=1 {
            match coord.move_coord(dy, dx, grid.len() as i64, grid[0].len() as i64) {
                Some(new_coord) if !path.contains(&amp;new_coord) =&gt; {
                    let mut new_path = path.clone();
                    new_path.push(new_coord);
                    let new_word = format!(
                        "{}{}",
                        &amp;word, &amp;grid[new_coord.row as usize][new_coord.col as usize]
                    );

                    boggle_solve_help(grid, dict, new_word, new_coord, new_path);
                }

                Some(_) =&gt; (),

                None =&gt; (),
            }
        }
    }
}

fn main() {
    let contents = fs::read_to_string("words_alpha.txt").unwrap();
    let dict = PrefixMap::new(contents.split_whitespace());

    let grid: Vec&lt;Vec&lt;char&gt;&gt; = vec![
        vec!['c', 's', 't', 'e', 't'],
        vec!['a', 'i', 'r', 'l', 's'],
        vec!['p', 'd', 'a', 'e', 's'],
        vec!['u', 'e', 'c', 's', 'e'],
        vec!['r', 'o', 't', 'r', 'i'],
    ];

    boggle_solve(&amp;grid, &amp;dict)
}


(playground - but note that words_alpha.txt doesn't exist, so you can't run it)