get_the_title() gets printed out twice

Your conditional is always true. To understand why, consider what happens in this function when there is no post parent:

function get_top_ancestor_id() {

    global $post;

    // no parent so this doesn't run:
    // if($post->post_parent) {
    //     $ancestors = array_reverse(get_post_ancestors($post->ID));
    //     return $ancestors[0];
    // }
    return $post->ID;
}

Which simplifies to:

function get_top_ancestor_id() {
    global $post;
    return $post->ID;
}

So if there is no parent post, it returns the ID of the current post, which means your calls simplify to this:

                echo get_the_title();
                echo get_the_title();

My recommendation is to change the get_top_ancestor_id so that it returns false, or use this code:

    global $post;
    if( $post->post_parent ) {
        echo get_the_title(get_top_ancestor_id());
    }
    echo get_the_title();

Security

This code does not escape its output! If an administrator copy pasted a script tag with dangerous code into a post title, it would render in the browser!

Lets fix that with esc_html:

    global $post;
    if( $post->post_parent ) {
        echo esc_html( get_the_title(get_top_ancestor_id()) );
    }
    echo esc_html( get_the_title() );

The code is now safe, and any dangerous content is broken, preventing its execution. Similarly, use esc_attr for attributes, esc_url for URLs, and wp_kses_post if you need images/links/headings/etc