Is get_posts() more efficient than The Loop?

When WordPress include the category.php (just like any other template) the main query (i.e. the query triggered by the url) is already ran: it means that a query on database was already fired and this is the reason why in a template you can run the Loop without calling a query.

If you call get_posts you ran another query to database, that is completely unnecessary once is identical to the one already ran by WordPress.

Also note, that get_posts is just a wrapper for WP_Query so the difference from using the Loop and using the foreach syntax, does not regard the query itself, but the way you use the result of the query.

If you prefer foreach over while (the Loop is just a while loop) you can use it also calling directly WP_Query:

$q = new WP_Query( $args ); // or $q = get_posts( $args );

foreach ( $q->posts as $post ) {
  // here your code e.g.
  $title = $post->title;
  $content = $post->content;
}

So why normally people use the Loop instead of this kind of loop? for 2 reason, at least:

  1. Setup of global $post object. When you call the_post() inside the Loop, that function globalize the variable $post containing the current post object. A lot of core functions and plugins rely on that variale, so often is important to setup. However, it can be set up also using a foreach loop, using setup_postdata function, the previous code becomes:

    global $post;
    foreach ( $q->posts as $post ) {
      setup_postdata($post);
      // here your code e.g.
      $title = $post->title;
      $content = $post->content;
    }
    
  2. Second reason regards template tags, over the direct access to post object properties: when you call the_title or the_content (or other template tags) filters are fired before output the property, and if you don’t call them, the output can be not the one you expect, again this can be done manually in our foreach loop:

    $q = new WP_Query( $args );
    global $post;
    foreach ( $q->posts as $post ) {
      setup_postdata($post);
      // here your code e.g.
      $title = apply_filters('the_title', $post->title);
      $content = apply_filters('the_content', $post->content);
    }
    

Last code block, can be considered almost identical to the Loop.

I said almost because there’s another thing that differe: 2 action hooks, loop_start and loop_end, fired by the Loop. Some plugins can use them, so for better compatibility you should do something like:

$q = new WP_Query( $args );

global $post;
do_action_ref_array('loop_start', array(&$q));
foreach ( $q->posts as $post ) {
  setup_postdata($post);
  // here your code e.g.
  $title = apply_filters('the_title', $post->title);
  $content = apply_filters('the_content', $post->content);
}
do_action_ref_array('loop_end', array(&$q));

As already said, this is better used for custom loops, but regarding the main query, calling another WP_Query is bad for performance and unnecessary. However, the main query is just a WP_Query instance, saved in the global variable $wp_query, so if in your template you do something like:

global $wp_query, $post;
do_action_ref_array('loop_start', array(&$wp_query));
foreach ( $wp_query->posts as $post ) {
  setup_postdata($post);
  // here your code e.g.
  $title = apply_filters('the_title', $post->title);
  $content = apply_filters('the_content', $post->content);
}
do_action_ref_array('loop_end', array(&$wp_query));

you are doing something very similar (pratically identical) to the Loop, just quite more verbose, but if you just hate while loops…

In conclusion, regarding main query, for compatibility reasons is advisable (but not mandatory) use the Loop: if you don’t trigger anction and filter hooks there are chance that if you install any plugins some of them will not work…

But for custom loops, sometimes, you don’t need (or you don’t want) to trigger action and filter hooks, and the foreach syntax can be preferable in that cases.