diff --git src/wp-includes/class-wp-comment-query.php src/wp-includes/class-wp-comment-query.php
index 33d9888..1371853 100644
--- src/wp-includes/class-wp-comment-query.php
+++ src/wp-includes/class-wp-comment-query.php
@@ -44,6 +44,22 @@ class WP_Comment_Query {
 	protected $meta_query_clauses;
 
 	/**
+	 * SQL query clauses.
+	 *
+	 * @since 4.4.0
+	 * @access protected
+	 * @var array
+	 */
+	protected $sql_clauses = array(
+		'select'  => '',
+		'from'    => '',
+		'where'   => array(),
+		'groupby' => '',
+		'orderby' => '',
+		'limits'  => '',
+	);
+
+	/**
 	 * Date query container
 	 *
 	 * @since 3.7.0
@@ -80,6 +96,24 @@ class WP_Comment_Query {
 	public $comments;
 
 	/**
+	 * The amount of found comments for the current query.
+	 *
+	 * @since 4.4.0
+	 * @access public
+	 * @var int
+	 */
+	public $found_comments = 0;
+
+	/**
+	 * The number of pages.
+	 *
+	 * @since 4.4.0
+	 * @access public
+	 * @var int
+	 */
+	public $max_num_pages = 0;
+
+	/**
 	 * Make private/protected methods readable for backwards compatibility.
 	 *
 	 * @since 4.0.0
@@ -103,7 +137,8 @@ class WP_Comment_Query {
 	 *
 	 * @since 4.2.0
 	 * @since 4.4.0 `$parent__in` and `$parent__not_in` were added.
-	 * @since 4.4.0 Order by `comment__in` was added. `$update_comment_meta_cache` was added.
+	 * @since 4.4.0 Order by `comment__in` was added. `$update_comment_meta_cache`, `$no_found_rows`, and
+	 *              `$hierarchical` were added.
 	 * @access public
 	 *
 	 * @param string|array $query {
@@ -132,6 +167,8 @@ class WP_Comment_Query {
 	 *     @type int          $number              Maximum number of comments to retrieve. Default null (no limit).
 	 *     @type int          $offset              Number of comments to offset the query. Used to build LIMIT clause.
 	 *                                             Default 0.
+	 *     @type bool         $no_found_rows       Whether to disable the `SQL_CALC_FOUND_ROWS` query.
+	 *                                             Default: true.
 	 *     @type string|array $orderby             Comment status or array of statuses. To use 'meta_value' or
 	 *                                             'meta_value_num', `$meta_key` must also be defined. To sort by
 	 *                                             a specific `$meta_query` clause, use that clause's array key.
@@ -170,6 +207,12 @@ class WP_Comment_Query {
 	 *     @type array        $type__in            Include comments from a given array of comment types. Default empty.
 	 *     @type array        $type__not_in        Exclude comments from a given array of comment types. Default empty.
 	 *     @type int          $user_id             Include comments for a specific user ID. Default empty.
+	 *     @type bool|string  $hierarchical        Whether to include comment descendants in the results.
+	 *                                             'threaded' returns a tree, with each comment's children stored
+	 *                                             in a `children` property on the `WP_Comment` object. 'flat'
+	 *                                             returns a flat array of found comments plus their children.
+	 *                                             This parameter is ignored when `$fields` is 'ids'.
+	 *                                             Accepts 'threaded', 'flat', or false. Default: false.
 	 *     @type bool         $update_comment_meta_cache Whether to prime the metadata cache for found comments.
 	 *                                                   Default true.
 	 * }
@@ -187,6 +230,7 @@ class WP_Comment_Query {
 			'karma' => '',
 			'number' => '',
 			'offset' => '',
+			'no_found_rows' => true,
 			'orderby' => '',
 			'order' => 'DESC',
 			'parent' => '',
@@ -212,6 +256,7 @@ class WP_Comment_Query {
 			'meta_value' => '',
 			'meta_query' => '',
 			'date_query' => null, // See WP_Date_Query
+			'hierarchical' => false,
 			'update_comment_meta_cache' => true,
 		);
 
@@ -314,6 +359,23 @@ class WP_Comment_Query {
 
 		$comment_ids = array_map( 'intval', $comment_ids );
 
+		$this->comment_count = count( $this->comments );
+
+		if ( $comment_ids && $this->query_vars['number'] && ! $this->query_vars['no_found_rows'] ) {
+			/**
+			 * Filter the query used to retrieve found comment count.
+			 *
+			 * @since 4.4.0
+			 *
+			 * @param string           $found_comments_query SQL query. Default 'SELECT FOUND_ROWS()'.
+			 * @param WP_Comment_Query $comment_query        The `WP_Comment_Query` instance.
+			 */
+			$found_comments_query = apply_filters( 'found_comments_query', 'SELECT FOUND_ROWS()', $this );
+			$this->found_comments = (int) $wpdb->get_var( $found_comments_query );
+
+			$this->max_num_pages = ceil( $this->found_comments / $this->query_vars['number'] );
+		}
+
 		if ( 'ids' == $this->query_vars['fields'] ) {
 			$this->comments = $comment_ids;
 			return $this->comments;
@@ -342,6 +404,10 @@ class WP_Comment_Query {
 		// Convert to WP_Comment instances
 		$comments = array_map( 'get_comment', $_comments );
 
+		if ( $this->query_vars['hierarchical'] ) {
+			$comments = $this->fill_descendants( $comments );
+		}
+
 		$this->comments = $comments;
 		return $this->comments;
 	}
@@ -357,9 +423,6 @@ class WP_Comment_Query {
 	protected function get_comment_ids() {
 		global $wpdb;
 
-		$groupby = '';
-		$where = array();
-
 		// Assemble clauses related to 'comment_approved'.
 		$approved_clauses = array();
 
@@ -423,9 +486,9 @@ class WP_Comment_Query {
 		// Collapse comment_approved clauses into a single OR-separated clause.
 		if ( ! empty( $approved_clauses ) ) {
 			if ( 1 === count( $approved_clauses ) ) {
-				$where[] = $approved_clauses[0];
+				$this->sql_clauses['where']['approved'] = $approved_clauses[0];
 			} else {
-				$where[] = '( ' . implode( ' OR ', $approved_clauses ) . ' )';
+				$this->sql_clauses['where']['approved'] = '( ' . implode( ' OR ', $approved_clauses ) . ' )';
 			}
 		}
 
@@ -524,8 +587,6 @@ class WP_Comment_Query {
 			} else {
 				$limits = 'LIMIT ' . $number;
 			}
-		} else {
-			$limits = '';
 		}
 
 		if ( $this->query_vars['count'] ) {
@@ -534,49 +595,47 @@ class WP_Comment_Query {
 			$fields = "$wpdb->comments.comment_ID";
 		}
 
-		$join = '';
-
 		$post_id = absint( $this->query_vars['post_id'] );
 		if ( ! empty( $post_id ) ) {
-			$where[] = $wpdb->prepare( 'comment_post_ID = %d', $post_id );
+			$this->sql_clauses['where']['post_id'] = $wpdb->prepare( 'comment_post_ID = %d', $post_id );
 		}
 
 		// Parse comment IDs for an IN clause.
 		if ( ! empty( $this->query_vars['comment__in'] ) ) {
-			$where[] = "$wpdb->comments.comment_ID IN ( " . implode( ',', wp_parse_id_list( $this->query_vars['comment__in'] ) ) . ' )';
+			$this->sql_clauses['where']['comment__in'] = "$wpdb->comments.comment_ID IN ( " . implode( ',', wp_parse_id_list( $this->query_vars['comment__in'] ) ) . ' )';
 		}
 
 		// Parse comment IDs for a NOT IN clause.
 		if ( ! empty( $this->query_vars['comment__not_in'] ) ) {
-			$where[] = "$wpdb->comments.comment_ID NOT IN ( " . implode( ',', wp_parse_id_list( $this->query_vars['comment__not_in'] ) ) . ' )';
+			$this->sql_clauses['where']['comment__not_in'] = "$wpdb->comments.comment_ID NOT IN ( " . implode( ',', wp_parse_id_list( $this->query_vars['comment__not_in'] ) ) . ' )';
 		}
 
 		// Parse comment parent IDs for an IN clause.
 		if ( ! empty( $this->query_vars['parent__in'] ) ) {
-			$where[] = 'comment_parent IN ( ' . implode( ',', wp_parse_id_list( $this->query_vars['parent__in'] ) ) . ' )';
+			$this->sql_clauses['where']['parent__in'] = 'comment_parent IN ( ' . implode( ',', wp_parse_id_list( $this->query_vars['parent__in'] ) ) . ' )';
 		}
 
 		// Parse comment parent IDs for a NOT IN clause.
 		if ( ! empty( $this->query_vars['parent__not_in'] ) ) {
-			$where[] = 'comment_parent NOT IN ( ' . implode( ',', wp_parse_id_list( $this->query_vars['parent__not_in'] ) ) . ' )';
+			$this->sql_clauses['where']['parent__not_in'] = 'comment_parent NOT IN ( ' . implode( ',', wp_parse_id_list( $this->query_vars['parent__not_in'] ) ) . ' )';
 		}
 
 		// Parse comment post IDs for an IN clause.
 		if ( ! empty( $this->query_vars['post__in'] ) ) {
-			$where[] = 'comment_post_ID IN ( ' . implode( ',', wp_parse_id_list( $this->query_vars['post__in'] ) ) . ' )';
+			$this->sql_clauses['where']['post__in'] = 'comment_post_ID IN ( ' . implode( ',', wp_parse_id_list( $this->query_vars['post__in'] ) ) . ' )';
 		}
 
 		// Parse comment post IDs for a NOT IN clause.
 		if ( ! empty( $this->query_vars['post__not_in'] ) ) {
-			$where[] = 'comment_post_ID NOT IN ( ' . implode( ',', wp_parse_id_list( $this->query_vars['post__not_in'] ) ) . ' )';
+			$this->sql_clauses['where']['post__not_in'] = 'comment_post_ID NOT IN ( ' . implode( ',', wp_parse_id_list( $this->query_vars['post__not_in'] ) ) . ' )';
 		}
 
 		if ( '' !== $this->query_vars['author_email'] ) {
-			$where[] = $wpdb->prepare( 'comment_author_email = %s', $this->query_vars['author_email'] );
+			$this->sql_clauses['where']['author_email'] = $wpdb->prepare( 'comment_author_email = %s', $this->query_vars['author_email'] );
 		}
 
 		if ( '' !== $this->query_vars['karma'] ) {
-			$where[] = $wpdb->prepare( 'comment_karma = %d', $this->query_vars['karma'] );
+			$this->sql_clauses['where']['karma'] = $wpdb->prepare( 'comment_karma = %d', $this->query_vars['karma'] );
 		}
 
 		// Filtering by comment_type: 'type', 'type__in', 'type__not_in'.
@@ -614,18 +673,22 @@ class WP_Comment_Query {
 
 			if ( ! empty( $comment_types[ $operator ] ) ) {
 				$types_sql = implode( ', ', $comment_types[ $operator ] );
-				$where[] = "comment_type $operator ($types_sql)";
+				$this->sql_clauses['where']['comment_type__' . strtolower( str_replace( ' ', '_', $operator ) ) ] = "comment_type $operator ($types_sql)";
 			}
 		}
 
+		if ( $this->query_vars['hierarchical'] && ! $this->query_vars['parent'] ) {
+			$this->query_vars['parent'] = 0;
+		}
+
 		if ( '' !== $this->query_vars['parent'] ) {
-			$where[] = $wpdb->prepare( 'comment_parent = %d', $this->query_vars['parent'] );
+			$this->sql_clauses['where']['parent'] = $wpdb->prepare( 'comment_parent = %d', $this->query_vars['parent'] );
 		}
 
 		if ( is_array( $this->query_vars['user_id'] ) ) {
-			$where[] = 'user_id IN (' . implode( ',', array_map( 'absint', $this->query_vars['user_id'] ) ) . ')';
+			$this->sql_clauses['where']['user_id'] = 'user_id IN (' . implode( ',', array_map( 'absint', $this->query_vars['user_id'] ) ) . ')';
 		} elseif ( '' !== $this->query_vars['user_id'] ) {
-			$where[] = $wpdb->prepare( 'user_id = %d', $this->query_vars['user_id'] );
+			$this->sql_clauses['where']['user_id'] = $wpdb->prepare( 'user_id = %d', $this->query_vars['user_id'] );
 		}
 
 		if ( '' !== $this->query_vars['search'] ) {
@@ -635,7 +698,7 @@ class WP_Comment_Query {
 			);
 
 			// Strip leading 'AND'.
-			$where[] = preg_replace( '/^\s*AND\s*/', '', $search_sql );
+			$this->sql_clauses['where']['search'] = preg_replace( '/^\s*AND\s*/', '', $search_sql );
 		}
 
 		// If any post-related query vars are passed, join the posts table.
@@ -648,41 +711,43 @@ class WP_Comment_Query {
 			foreach ( $post_fields as $field_name => $field_value ) {
 				// $field_value may be an array.
 				$esses = array_fill( 0, count( (array) $field_value ), '%s' );
-				$where[] = $wpdb->prepare( " {$wpdb->posts}.{$field_name} IN (" . implode( ',', $esses ) . ')', $field_value );
+				$this->sql_clauses['where']['post_fields'] = $wpdb->prepare( " {$wpdb->posts}.{$field_name} IN (" . implode( ',', $esses ) . ')', $field_value );
 			}
 		}
 
 		// Comment author IDs for an IN clause.
 		if ( ! empty( $this->query_vars['author__in'] ) ) {
-			$where[] = 'user_id IN ( ' . implode( ',', wp_parse_id_list( $this->query_vars['author__in'] ) ) . ' )';
+			$this->sql_clauses['where']['author__in'] = 'user_id IN ( ' . implode( ',', wp_parse_id_list( $this->query_vars['author__in'] ) ) . ' )';
 		}
 
 		// Comment author IDs for a NOT IN clause.
 		if ( ! empty( $this->query_vars['author__not_in'] ) ) {
-			$where[] = 'user_id NOT IN ( ' . implode( ',', wp_parse_id_list( $this->query_vars['author__not_in'] ) ) . ' )';
+			$this->sql_clauses['where']['author__not_in'] = 'user_id NOT IN ( ' . implode( ',', wp_parse_id_list( $this->query_vars['author__not_in'] ) ) . ' )';
 		}
 
 		// Post author IDs for an IN clause.
 		if ( ! empty( $this->query_vars['post_author__in'] ) ) {
 			$join_posts_table = true;
-			$where[] = 'post_author IN ( ' . implode( ',', wp_parse_id_list( $this->query_vars['post_author__in'] ) ) . ' )';
+			$this->sql_clauses['where']['post_author__in'] = 'post_author IN ( ' . implode( ',', wp_parse_id_list( $this->query_vars['post_author__in'] ) ) . ' )';
 		}
 
 		// Post author IDs for a NOT IN clause.
 		if ( ! empty( $this->query_vars['post_author__not_in'] ) ) {
 			$join_posts_table = true;
-			$where[] = 'post_author NOT IN ( ' . implode( ',', wp_parse_id_list( $this->query_vars['post_author__not_in'] ) ) . ' )';
+			$this->sql_clauses['where']['post_author__not_in'] = 'post_author NOT IN ( ' . implode( ',', wp_parse_id_list( $this->query_vars['post_author__not_in'] ) ) . ' )';
 		}
 
+		$join = '';
+
 		if ( $join_posts_table ) {
-			$join = "JOIN $wpdb->posts ON $wpdb->posts.ID = $wpdb->comments.comment_post_ID";
+			$join .= "JOIN $wpdb->posts ON $wpdb->posts.ID = $wpdb->comments.comment_post_ID";
 		}
 
 		if ( ! empty( $this->meta_query_clauses ) ) {
 			$join .= $this->meta_query_clauses['join'];
 
 			// Strip leading 'AND'.
-			$where[] = preg_replace( '/^\s*AND\s*/', '', $this->meta_query_clauses['where'] );
+			$this->sql_clauses['where']['meta_query'] = preg_replace( '/^\s*AND\s*/', '', $this->meta_query_clauses['where'] );
 
 			if ( ! $this->query_vars['count'] ) {
 				$groupby = "{$wpdb->comments}.comment_ID";
@@ -692,10 +757,10 @@ class WP_Comment_Query {
 		$date_query = $this->query_vars['date_query'];
 		if ( ! empty( $date_query ) && is_array( $date_query ) ) {
 			$date_query_object = new WP_Date_Query( $date_query, 'comment_date' );
-			$where[] = preg_replace( '/^\s*AND\s*/', '', $date_query_object->get_sql() );
+			$this->sql_clauses['where']['date_query'] = preg_replace( '/^\s*AND\s*/', '', $date_query_object->get_sql() );
 		}
 
-		$where = implode( ' AND ', $where );
+		$where = implode( ' AND ', $this->sql_clauses['where'] );
 
 		$pieces = array( 'fields', 'join', 'where', 'orderby', 'limits', 'groupby' );
 		/**
@@ -727,7 +792,18 @@ class WP_Comment_Query {
 			$orderby = "ORDER BY $orderby";
 		}
 
-		$this->request = "SELECT $fields FROM $wpdb->comments $join $where $groupby $orderby $limits";
+		$found_rows = '';
+		if ( ! $this->query_vars['no_found_rows'] ) {
+			$found_rows = 'SQL_CALC_FOUND_ROWS';
+		}
+
+		$this->sql_clauses['select']  = "SELECT $found_rows $fields";
+		$this->sql_clauses['from']    = "FROM $wpdb->comments $join";
+		$this->sql_clauses['groupby'] = $groupby;
+		$this->sql_clauses['orderby'] = $orderby;
+		$this->sql_clauses['limits']  = $limits;
+
+		$this->request = "{$this->sql_clauses['select']} {$this->sql_clauses['from']} {$where} {$this->sql_clauses['groupby']} {$this->sql_clauses['orderby']} {$this->sql_clauses['limits']}";
 
 		if ( $this->query_vars['count'] ) {
 			return intval( $wpdb->get_var( $this->request ) );
@@ -738,6 +814,81 @@ class WP_Comment_Query {
 	}
 
 	/**
+	 * Fetch descendants for located comments.
+	 *
+	 * @since 4.4.0
+	 *
+	 * @param array $comments Array of top-level comments whose descendants should be filled in.
+	 * @return array
+	 */
+	protected function fill_descendants( $comments ) {
+		global $wpdb;
+
+		$levels = array(
+			0 => wp_list_pluck( $comments, 'comment_ID' ),
+		);
+
+		$where_clauses = $this->sql_clauses['where'];
+		unset(
+			$where_clauses['parent'],
+			$where_clauses['parent__in'],
+			$where_clauses['parent__not_in']
+		);
+
+		// Fetch an entire level of the descendant tree at a time.
+		$level = 0;
+		do {
+			$parent_ids = $levels[ $level ];
+			$where = 'WHERE ' . implode( ' AND ', $where_clauses ) . ' AND comment_parent IN (' . implode( ',', array_map( 'intval', $parent_ids ) ) . ')';
+			$comment_ids = $wpdb->get_col( "{$this->sql_clauses['select']} {$this->sql_clauses['from']} {$where} {$this->sql_clauses['groupby']}" );
+
+			$level++;
+			$levels[ $level ] = $comment_ids;
+		} while ( $comment_ids );
+
+		// Prime comment caches for non-top-level comments.
+		$descendant_ids = array();
+		for ( $i = 1; $i < count( $levels ); $i++ ) {
+			$descendant_ids = array_merge( $descendant_ids, $levels[ $i ] );
+		}
+
+		_prime_comment_caches( $descendant_ids, $this->query_vars['update_comment_meta_cache'] );
+
+		// Assemble a flat array of all comments + descendants.
+		$all_comments = $comments;
+		foreach ( $descendant_ids as $descendant_id ) {
+			$all_comments[] = get_comment( $descendant_id );
+		}
+
+		// If a threaded representation was requested, build the tree.
+		if ( 'threaded' === $this->query_vars['hierarchical'] ) {
+			$threaded_comments = $ref = array();
+			foreach ( $all_comments as $k => $c ) {
+				$_c = get_comment( $c->comment_ID );
+
+				// If the comment isn't in the reference array, it goes in the top level of the thread.
+				if ( ! isset( $ref[ $c->comment_parent ] ) ) {
+					$threaded_comments[ $_c->comment_ID ] = $_c;
+					$ref[ $_c->comment_ID ] = $threaded_comments[ $_c->comment_ID ];
+
+				// Otherwise, set it as a child of its parent.
+				} else {
+
+					$ref[ $_c->comment_parent ]->add_child( $_c );
+//					$ref[ $c->comment_parent ]->children[ $c->comment_ID ] = $c;
+					$ref[ $_c->comment_ID ] = $ref[ $_c->comment_parent ]->get_child( $_c->comment_ID );
+				}
+			}
+
+			$comments = $threaded_comments;
+		} else {
+			$comments = $all_comments;
+		}
+
+		return $comments;
+	}
+
+	/**
 	 * Used internally to generate an SQL string for searching across multiple columns
 	 *
 	 * @since 3.1.0
diff --git src/wp-includes/class-wp-comment.php src/wp-includes/class-wp-comment.php
index 581fca2..b1d84b5 100644
--- src/wp-includes/class-wp-comment.php
+++ src/wp-includes/class-wp-comment.php
@@ -150,6 +150,15 @@ final class WP_Comment {
 	public $user_id = 0;
 
 	/**
+	 * Comment children.
+	 *
+	 * @since 4.4.0
+	 * @access protected
+	 * @var array
+	 */
+	protected $children;
+
+	/**
 	 * Retrieves a WP_Comment instance.
 	 *
 	 * @since 4.4.0
@@ -211,4 +220,55 @@ final class WP_Comment {
 	public function to_array() {
 		return get_object_vars( $this );
 	}
+
+	/**
+	 * Get the children of a comment.
+	 *
+	 * @since 4.4.0
+	 * @access public
+	 *
+	 * @return array Array of `WP_Comment` objects.
+	 */
+	public function get_children() {
+		if ( is_null( $this->children ) ) {
+			$this->children = get_comments( array(
+				'parent' => $this->comment_ID,
+				'hierarchical' => 'threaded',
+			) );
+		}
+
+		return $this->children;
+	}
+
+	/**
+	 * Add a child to the comment.
+	 *
+	 * Used by `WP_Comment_Query` when bulk-filling descendants.
+	 *
+	 * @since 4.4.0
+	 * @access public
+	 *
+	 * @param WP_Comment $child Child comment.
+	 */
+	public function add_child( WP_Comment $child ) {
+		$this->comments[ $child->comment_ID ] = $child;
+	}
+
+	/**
+	 * Get a child comment by ID.
+	 *
+	 * @since 4.4.0
+	 * @access public
+	 *
+	 * @param int $child_id ID of the child.
+	 * @return WP_Comment|bool Returns the comment object if found, otherwise false.
+	 */
+	public function get_child( $child_id ) {
+		if ( isset( $this->comments[ $child_id ] ) ) {
+			return $this->comments[ $child_id ];
+		}
+
+		return false;
+	}
 }
+
diff --git src/wp-includes/comment-template.php src/wp-includes/comment-template.php
index 79b51c1..9650d93 100644
--- src/wp-includes/comment-template.php
+++ src/wp-includes/comment-template.php
@@ -1211,6 +1211,8 @@ function comments_template( $file = '/comments.php', $separate_comments = false
 		'orderby' => 'comment_date_gmt',
 		'status'  => 'approve',
 		'post_id' => $post->ID,
+		'hierarchical' => 'flat',
+		'no_found_rows' => false,
 		'update_comment_meta_cache' => false, // We lazy-load comment meta for performance.
 	);
 
@@ -1220,7 +1222,43 @@ function comments_template( $file = '/comments.php', $separate_comments = false
 		$comment_args['include_unapproved'] = array( $comment_author_email );
 	}
 
-	$comments = get_comments( $comment_args );
+	$per_page = (int) get_query_var( 'comments_per_page' );
+	if ( 0 === $per_page ) {
+		$per_page = (int) get_option( 'comments_per_page' );
+	}
+
+	$paging_threshold = $per_page ? $per_page * 2 : 100;
+
+	/**
+	 * Filter the threshold at which comment pagination is forced.
+	 *
+	 * Pages with very large numbers of comments do not perform well, so we force pagination when the comment count
+	 * exceeds a certain threshold. Use this filter to change the default threshold.
+	 *
+	 * @since 4.4.0
+	 *
+	 * @param int     $paging_threshold Number of comments a post must have before pagination is forced.
+	 * @param WP_Post $post             The WP_Post instance.
+	 */
+	$paging_threshold = apply_filters( 'force_comment_paging_threshold', $paging_threshold, $post );
+
+	if ( (int) get_option( 'page_comments' ) || $post->comment_count > $paging_threshold ) {
+		// Force pagination to be turned on in theme comment navigation functions.
+		add_filter( 'pre_option_page_comments', '__return_true' );
+
+		if ( $per_page ) {
+			$comment_args['number'] = $per_page;
+		} else {
+			$comment_args['number'] = $paging_threshold / 2;
+		}
+
+		$page = (int) get_query_var( 'cpage' );
+		if ( $page ) {
+			$comment_args['offset'] = ( $page - 1 ) * $per_page;
+		}
+	}
+
+	$comment_query = new WP_Comment_Query( $comment_args );
 
 	/**
 	 * Filter the comments array.
@@ -1230,9 +1268,10 @@ function comments_template( $file = '/comments.php', $separate_comments = false
 	 * @param array $comments Array of comments supplied to the comments template.
 	 * @param int   $post_ID  Post ID.
 	 */
-	$wp_query->comments = apply_filters( 'comments_array', $comments, $post->ID );
+	$wp_query->comments = apply_filters( 'comments_array', $comment_query->comments, $post->ID );
 	$comments = &$wp_query->comments;
 	$wp_query->comment_count = count($wp_query->comments);
+	$wp_query->max_num_comment_pages = $comment_query->max_num_pages;
 
 	if ( $separate_comments ) {
 		$wp_query->comments_by_type = separate_comments($comments);
@@ -1816,6 +1855,11 @@ function wp_list_comments( $args = array(), $comments = null ) {
 		} else {
 			$_comments = $wp_query->comments;
 		}
+
+		// Pagination is already handled by `WP_Comment_Query`, so we tell Walker not to bother.
+		if ( 1 < $wp_query->max_num_comment_pages ) {
+			$r['page'] = 1;
+		}
 	}
 
 	if ( '' === $r['per_page'] && get_option('page_comments') )
@@ -1857,7 +1901,6 @@ function wp_list_comments( $args = array(), $comments = null ) {
 	}
 
 	$output = $walker->paged_walk( $_comments, $r['max_depth'], $r['page'], $r['per_page'], $r );
-	$wp_query->max_num_comment_pages = $walker->max_pages;
 
 	$in_comment_loop = false;
 
diff --git tests/phpunit/tests/comment.php tests/phpunit/tests/comment.php
index a20f58d..80a7e65 100644
--- tests/phpunit/tests/comment.php
+++ tests/phpunit/tests/comment.php
@@ -273,4 +273,55 @@ class Tests_Comment extends WP_UnitTestCase {
 		$sent = wp_new_comment_notify_postauthor( $c );
 		$this->assertFalse( $sent );
 	}
+
+	/**
+	 * @ticket 8071
+	 */
+	public function test_wp_comment_get_children_should_fill_children() {
+
+		$p = $this->factory->post->create();
+
+		$c1 = $this->factory->comment->create( array(
+			'comment_post_ID' => $p,
+			'comment_approved' => '1',
+		) );
+
+		$c2 = $this->factory->comment->create( array(
+			'comment_post_ID' => $p,
+			'comment_approved' => '1',
+			'comment_parent' => $c1,
+		) );
+
+		$c3 = $this->factory->comment->create( array(
+			'comment_post_ID' => $p,
+			'comment_approved' => '1',
+			'comment_parent' => $c2,
+		) );
+
+		$c4 = $this->factory->comment->create( array(
+			'comment_post_ID' => $p,
+			'comment_approved' => '1',
+			'comment_parent' => $c1,
+		) );
+
+		$c5 = $this->factory->comment->create( array(
+			'comment_post_ID' => $p,
+			'comment_approved' => '1',
+		) );
+
+		$c6 = $this->factory->comment->create( array(
+			'comment_post_ID' => $p,
+			'comment_approved' => '1',
+			'comment_parent' => $c5,
+		) );
+
+		$comment = get_comment( $c1 );
+		$children = $comment->get_children();
+
+		// Direct descendants of $c1.
+		$this->assertEquals( array( $c2, $c4 ), array_values( wp_list_pluck( $children, 'comment_ID' ) ) );
+
+		// Direct descendants of $c2.
+		$this->assertEquals( array( $c3 ), array_values( wp_list_pluck( $children[ $c2 ]->get_children(), 'comment_ID' ) ) );
+	}
 }
diff --git tests/phpunit/tests/comment/query.php tests/phpunit/tests/comment/query.php
index 73ab4aa..f90857b 100644
--- tests/phpunit/tests/comment/query.php
+++ tests/phpunit/tests/comment/query.php
@@ -1873,4 +1873,126 @@ class Tests_Comment_Query extends WP_UnitTestCase {
 		$this->assertEquals( array( $c2, $c3 ), $ids->comments );
 
 	}
+
+	/**
+	 * @ticket 8071
+	 */
+	public function test_hierarchical_should_skip_child_comments_in_offset() {
+		$top_level_0 = $this->factory->comment->create( array(
+			'comment_post_ID' => $this->post_id,
+			'comment_approved' => '1',
+		) );
+
+		$child_of_0 = $this->factory->comment->create( array(
+			'comment_post_ID' => $this->post_id,
+			'comment_approved' => '1',
+			'comment_parent' => $top_level_0,
+		) );
+
+		$top_level_comments = $this->factory->comment->create_many( 3, array(
+			'comment_post_ID' => $this->post_id,
+			'comment_approved' => '1',
+		) );
+
+		$q = new WP_Comment_Query( array(
+			'post_id' => $this->post_id,
+			'hierarchical' => 'flat',
+			'number' => 2,
+			'offset' => 1,
+			'orderby' => 'comment_ID',
+			'order' => 'ASC',
+			'fields' => 'ids',
+		) );
+
+		$this->assertEquals( array( $top_level_comments[0], $top_level_comments[1] ), $q->comments );
+	}
+
+	/**
+	 * @ticket 8071
+	 */
+	public function test_hierarchical_should_not_include_child_comments_in_number() {
+		$top_level_0 = $this->factory->comment->create( array(
+			'comment_post_ID' => $this->post_id,
+			'comment_approved' => '1',
+		) );
+
+		$child_of_0 = $this->factory->comment->create( array(
+			'comment_post_ID' => $this->post_id,
+			'comment_approved' => '1',
+			'comment_parent' => $top_level_0,
+		) );
+
+		$top_level_comments = $this->factory->comment->create_many( 3, array(
+			'comment_post_ID' => $this->post_id,
+			'comment_approved' => '1',
+		) );
+
+		$q = new WP_Comment_Query( array(
+			'post_id' => $this->post_id,
+			'hierarchical' => 'flat',
+			'number' => 2,
+			'orderby' => 'comment_ID',
+			'order' => 'ASC',
+		) );
+
+		$this->assertEqualSets( array( $top_level_0, $child_of_0, $top_level_comments[0] ), wp_list_pluck( $q->comments, 'comment_ID' ) );
+	}
+
+	/**
+	 * @ticket 8071
+	 */
+	public function test_hierarchical_threaded() {
+		$c1 = $this->factory->comment->create( array(
+			'comment_post_ID' => $this->post_id,
+			'comment_approved' => '1',
+		) );
+
+		$c2 = $this->factory->comment->create( array(
+			'comment_post_ID' => $this->post_id,
+			'comment_approved' => '1',
+			'comment_parent' => $c1,
+		) );
+
+		$c3 = $this->factory->comment->create( array(
+			'comment_post_ID' => $this->post_id,
+			'comment_approved' => '1',
+			'comment_parent' => $c2,
+		) );
+
+		$c4 = $this->factory->comment->create( array(
+			'comment_post_ID' => $this->post_id,
+			'comment_approved' => '1',
+			'comment_parent' => $c1,
+		) );
+
+		$c5 = $this->factory->comment->create( array(
+			'comment_post_ID' => $this->post_id,
+			'comment_approved' => '1',
+		) );
+
+		$c6 = $this->factory->comment->create( array(
+			'comment_post_ID' => $this->post_id,
+			'comment_approved' => '1',
+			'comment_parent' => $c5,
+		) );
+
+		$q = new WP_Comment_Query( array(
+			'post_id' => $this->post_id,
+			'hierarchical' => 'threaded',
+			'orderby' => 'comment_ID',
+			'order' => 'ASC',
+		) );
+
+		// Top-level comments.
+		$this->assertEquals( array( $c1, $c5 ), array_values( wp_list_pluck( $q->comments, 'comment_ID' ) ) );
+
+		// Direct descendants of $c1.
+		$this->assertEquals( array( $c2, $c4 ), array_values( wp_list_pluck( $q->comments[ $c1 ]->get_children(), 'comment_ID' ) ) );
+
+		// Direct descendants of $c2.
+		$this->assertEquals( array( $c3 ), array_values( wp_list_pluck( $q->comments[ $c1 ]->get_child( $c2 )->get_children(), 'comment_ID' ) ) );
+
+		// Direct descendants of $c5.
+		$this->assertEquals( array( $c6 ), array_values( wp_list_pluck( $q->comments[ $c5 ]->get_children(), 'comment_ID' ) ) );
+	}
 }
