Your latest statements are more or less correct, but it appears you do not truly understand how these various ways of altering page results really work. I'll try to explain, which I hope will serve your better than brief answers with no explanation.
You probably know how a basic request works in WP, but I'll go over it so we are starting on the same ground. A user types an URL or clicks a link with an URL in their browser. This HTTP request is sent to your server. The server tries to find an associated file, which if the request is for WP content, the search will fail, so the request is sent to WP's index.php. This causes WP to parse the URL and based on what it finds in the URL, assigns the values to various query variables, or "query vars". The query vars are used to build a SQL query that will best retrieve the records matching the URL parameters. The DB is queried, the results returned, and displayed via the "Loop".
add_query_arg() does is modify the link a user clicks. Thus it is what actually initiates the request so it is the very earliest possible modification of the request. The added query args (added to the URL) are assigned to query vars (belonging to the query object), which are then used to construct a query, so the returned results reflect these added args. Note that query args and query vars, while related, each belong to very different parts of the process.
'pre_get_posts' occurs after the query vars are assigned, but before the SQL query is built and the DB queried. Thus it occurs much later than
add_query_arg() and can countermand what was added with
add_query_arg() occurs earlier than 'pre_get_posts', it cannot possibly change what 'pre_get_posts' does.
By themselves, either method can easily alter the query just as well. Together, the methods can either contradict or supplement each other, depending on what they are trying to do. In case of conflict, 'pre_get_posts' always has final say.
You can now see why
add_query_arg() could never possibly change what's done by 'pre_get_posts'. You should also see that
add_query_arg() could be used for a modified default view, but 'pre_get_posts' is probably better because it has final say. However, the callback must look for any changes from the unmodified query vars and respect them, as they would represent specific user requests. Without checking for changes, 'pre_get_posts' would be totalitarian and not allow any variations from what it does, as it has the final say. That kind of power comes with responsibility.
You can also see neither of these runs a new query, they both influence the query before it is even assembled. Thus either of these are preferable to methods that discard the main query and rerun a new one, such as
get_posts() or creating a new WP_Query object after the main query is run. These methods do have their place when additional data is need to supplement the main query, but should not be used in place of the main query.
It's impossible to say which approach is faster because action hooks are highly variable based on how complex the callback functions are. At best, 'pre_get_posts' can be extremely fast, or it could bog down with recursive walker functions.
add_query_arg() does have some overhead that runs each time it is called. It's not much, but if a lot of args are added, it could add up. I prefer 'pre_get_posts' for changing default behavior and
add_query_arg() for user preferences simply because the resulting URLs are aligned with user expectations. The speed difference is typically negligible.
This entire concept is important to understanding how WP really works, so if you are still not clear on something, feel free to ask for clarification.